Как стать автором
Обновить

Комментарии 17

Контравариантность не вписывается в LSP

Ещё как вписывается. Только нужно понимать её правильно, а не так как её понимаете вы.


Ошибка — вот в этой картинке:


image

Тип "коробка для питомца" не является подтипом для "коробки для животного". Эти типы не являются совместимыми друг с другом ни в одну сторону. Иными словами, понятие "коробка" инвариантно.


Конечно же, вы, как программист, вольны нарушить LSP и обойти это ограничение. Но в таком случае система типов языка перестанет защищать вас от ошибок.


Однако, можно выделить два семейства интерфейсов, наподобие вот таких:


interface TakeOnlyBox<T> {
    take() : T;
}

interface PutOnlyBox<T> {
    put(item: T);
}

Первый интерфейс образует иерархию подтипов, аналогичную показанной на картинке — то есть TakeOnlyBox<Pet> является подтипом TakeOnlyBox<Animal>. Такое семейство типов называется ковариантным.


Второй интерфейс образует иерархию подтипов, "развёрнутую" в обратную сторону: PutOnlyBox<Pet> является супертипом PutOnlyBox<Animal>. Такое семейство типов называется контравариантным.


Так вот, если функция pushPet с картинки будет принимать не просто коробку, а интерфейс PutOnlyBox<Pet> — это никак не нарушит LSP.

Прям с языка сняли…

Отношение подтипов и совместимость типов — разные понятия. Фактически лсп утверждает, что второе должно совпадать с первым. Однако, в статье показано, что это не так.


Инвариантность дженериков — это костыль в некоторых языках. Не распространяйте его на всё программиронание. В статье на которую я дал ссылку вы можете найти код на языке D, который умеет правильно выводить вариантность (совместимость). Без глупостей про тотальную инвариантность дженериков.

Если вы считаете, что в "клетку для собак" нельзя сажать других животных, то такую клетку нельзя считать подтипом "клетки для животных". Подтип должен уметь делать всё то же, что и родитель. Например-вмещать в себя кошек.
Комментатор выше то же самое написал, только на чуть другом языке.
P.s. с вашей точки зрения, "коробка для животных с собакой внутри" и "коробка для собак с собакой внутри" — это одно и то же или нет?

И что мы должны увидеть по вашей ссылке? Если вы сейчас про от это:


The second method, called depth subtyping, replaces the various fields with their subtypes.

То не забывайте прочитать немного дальше:


Depth subtyping only makes sense for immutable records
>А вот супертип очень даже можно, ведь в клетку для животных можно засовывать хоть домашнего, хоть дикого животного.
А кто вам сказал, что клетка для любого животного является подтипом (ну или супертипом) коробки для домашнего? Вообще говоря, это не определено.

И что мы должны увидеть по вашей ссылке?

>Инвариантность дженериков — это костыль в некоторых языках.
Я лишь хотел отметить, что в некоторых языках бывает по-разному. И этот вопрос в общем надо было рассмотреть до того, как утверждать, что клетка для собак это подтип клетки для животных.

Когда я увидел название, подумал, что будет про Language Server Protocol (протокол, с помощью которого взаимодействуют, например, VScode и Rust Language Server, отделяет редактор от программы, которая генерирует подсказки, проверяет код на ошибки и т.д.).

НЛО прилетело и опубликовало эту надпись здесь
супертипом которых, по-вашему, являются натуральные числа

Хм, а это откуда? В статье я такого не вижу.

НЛО прилетело и опубликовало эту надпись здесь

Поставил плюс за подъём темы в варианте, о котором раньше не приходилось задумываться — как-то просто LSP и вопросы ковариатности/контравариантности не возникали одновременно в одном контексте. Видимо, это просто инерция мышления (LSP обычно формулируется в терминах, соответствующих "входным" сущностям и именно на входе, но "ковариантность" там не звучит).


"Коробка", я понимаю, была аллюзией на коллекцию и/или проблемы вариантности для шаблонных типов (включая коллекции), как раз, два? (Специально взял первые попавшиеся ссылки в поиске.) Мне такое условное представление не очень адекватно, но я его по крайней мере понял.


С другой стороны, тут просто переформулировкой можно исключить ту же проблему. Да, например, вы можете Cat вложить в Box[Cat], а можете в Box[Pet] или Box[Animal]. Но: с точки зрения того, что происходит в пределах формализованного интерфейса/контракта одной функции, как раз LSP не нарушает. Если она возвращает Cat то уже дело принимающего, что он ждёт Pet или Animal. Просто в варианте, когда как, например, в Java/C# мы получаем параметром Box<? super Cat> и вкладываем в него выходное значение, фактически мы рассматриваем тот же LSP, но за счёт специфики оформления интерфейса вместо выходного значения функции мы получаем входное — и для принимающего всё равно выполняется LSP, только для каждого по-своему: кто-то ждал Pet и получил Cat, кто-то ждал Animal и получил Cat.


На "деконструкцию", конечно, не тянет. Псевдо-инверсия (псевдо — потому что на формальном уровне, сохраняя фактический смысл). Но было полезно продумать ещё раз.
Или я какой-то ещё вариант применения упустил?

Коллекции - частный случай. Коробка в примерах - это любой объект с мутабельным полем.

А мутирующая функция ничего не возвращает. Она просто меняет содержимое переданной ей коробки.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации