(←) предыдущая запись ; следующая запись (→)
программистское, образовательное
На самом деле, я хотел написать коротенький текст про нотацию, на сей раз программистскую. Этот текст там (↓). Но чтобы сделать текст понятным мне пришлось написать большую подводку для тех, кто не знаком с концепцией ко-, контра- и инвариантного наследования типов. А в идеале и для тех, кто совсем не программирует. Enjoy!
Кстати, мне было бы интересно узнать, насколько это понятно для людей вне области, и есть ли для вас в этом хоть что-то интересное. Давайте считать, что реакции 👌🤷♀️🥱 зарезервированы для «всё ок», «ничего не понятно» и «скука смертная» среди не-программистов.
Кажется, я как-то не так использую телеграм, но вот.
———
В Java есть так называемые дженерики (generics) — «обобщённые» или «параметризованные» типы. Например, множество чисел будет записываться как Set<Integer>
, а множество строк как Set<String>
. Здесь Integer
— это что-то вроде базового типа.
Часто бывает полезно оперировать множествами значений одного и того же абстрактного типа T
, обозначим это Set<T>
. Во многих случаях мы можем писать код, не задумываясь о том, что именно представляет из себя T
.
Это позволяет писать меньше кода за счёт того, что он теперь более универсальный: сортировка чисел и сортировка строк будут делаться одной и той же функцией sort(Set<T>)
, которая работает с множеством объектов любого типа.
Мы должны были бы дополнительно указать, что объекты должны быть сравниваемыми (т.е. тип T должен быть Comparable), но опустим эту деталь.
С дженериками тесно связано понятие вариантности. Она бывает трёх видов: ковариантность, контравариантность и инвариантность. Вариантность описывает, как наследуются обобщённые типы.
Например, у нас есть два типа: тексты (Text
) и стихи (Poem
).
Очевидно, стихотворение является текстом (мы здесь упростим реальности и опустим тот факт, что стихотворение может быть и не текстом). Обозначим такое отношение типов стрелочкой: Poem ← Text
и будем говорить, что тип Poem
унаследован от типа Text
.
В таких случаях ещё говорят, что Poem
— подтип, а Text
— родительский тип или надтип.
Теперь заведём два обобщённых типа: автор и читатель (или скорее чтец). Более общо они называются производитель (producer) и потребитель (consumer).
type Author<T> {
// Функция без аргументов, при вызове возращает
// художественное произведение типа T
T create_masterpiece();
}
type Reader<T> {
// Функция принимает текст типа T и
// громко и с выражением его зачитывает
read_out(T some_text)
}
И автор, и чтец у нас на зарплате («Власти оказались хитрей — они наняли зрителей») , так что если автор заявляет, что умеет писать стихи, то должен суметь по заказу написать стихотворение. А читатель текстов точно так же по заказу должен уметь прочитать любой текст, какой его попросят.
Автор — это обобщённый производитель. Он может производить стихотворения, а может — романы, комиксы, ноты или просто какие-то обобщённые тексты.
Обратите внимание, что автор стихотворений является автором текстов, а вот в обратную сторону это неверно: не любой создатель текстов — поэт. Таким образом, Author<Poem> ← Author<Text>
Чтецы тоже бывают привередливыми: некоторые из них читают только стихи. А бывают и всеядными, которым всё равно, что читать (они могут читать даже новости, хоть и через слёзы).
Теперь следите за руками! Читатель поэзии НЕ является читателем текстов, потому что ему нельзя «скормить» произвольный текст. А вот читатель произвольных текстов является читателем и поэзии тоже: Reader<Text> ← Reader<Poem>
Заметили разницу? Эти два разных типа наследований называются ковариантным и контравариантным. Давайте выпишу рядом, как отношение наследования типов T
и S
переносится на производные типы Reader<T>
, Reader<S>
, Author<T>
, Author<S>
.
Poem ← Text базовый тип
Author<Poem> ← Author<Text> ковариантное наследование
Reader<Poem> → Reader<Text> контравариантное наследование
В ковариантном наследовании направление стрелочки сохраняется, в контравариантном она меняет направление.
(1/4)