(←) предыдущая запись ; следующая запись (→)

программистское, образовательное

На самом деле, я хотел написать коротенький текст про нотацию, на сей раз программистскую. Этот текст там (↓). Но чтобы сделать текст понятным мне пришлось написать большую подводку для тех, кто не знаком с концепцией ко-, контра- и инвариантного наследования типов. А в идеале и для тех, кто совсем не программирует. 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)