LINUX.ORG.RU

Ковариантность и динамическая типизация

 ,


0

1

Согласно принципу Лисков, экземпляр подтипа должен вести себя так же, как экземпляр супертипа. И вот у нас Keyboard - подтип Device, мы делаем какой-нибудь List<Device> (как бы супертип) и List<Keyboard> (как бы подтип).

Но на самом деле нет. Если мы рассмотрим полную сигнатуру функции «добавить» с учётом динамической типизации, то оказывается, что попытка вставить Mouse в List<Keyboard> имеет определённое поведение, отличающееся от такового для List<Device>. В первом случае будет ошибка времени выполнения (некорректный тип), а во втором мышь вставится в список.

Т.е. здесь нет ковариантности.

А теперь вопрос: что используется в ковариантности в языках, позволяющих скомпилировать код, вставляющий «нечто» (any,variant,t) в такой вот «псевдоковариантный» контейнер.

Мне кажется, должен быть какой-то костыль для этой ситуации, и я подозреваю, что он может присутствовать в Scala и/или в C#. Кто может что-нибудь сказать?

★★★★★

А теперь вопрос: что используется в ковариантности в языках, позволяющих скомпилировать код, вставляющий «нечто» (any,variant,t) в такой вот «псевдоковариантный» контейнер.

Ничего не используется, List<T> не ковариантен. Ковариантен, например, IEnumerable<T>, что логично везде где требуется IEnumerable<Device> можно использовать IEnumerable<Keyboard>.

anonymous ()

Шёл 2017й год, а den73 продолжал троллить за CL :)

yoghurt ★★★★★ ()

Оригинальное определение LSP не содержит упоминания о поведении.

Список устройств и список клавиатур это просто два списка — список любых устройств и список конкретных устройств.

Minona ()

Ты про коллекцию или про функцию? Непонятно из написанного тобою.

Для коллекций обычно действует ковариантность, а вот для функций - уже «контравариантность», когда наследник и предок меняются местами.

Посмотреть можно в учебниках по Scala

dave ★★★★★ ()
Ответ на: комментарий от dave

Ты про коллекцию или про функцию? Непонятно из написанного тобою.

Коллекция, у которой есть метод «добавить».

monk ★★★★★ ()
Ответ на: комментарий от den73

что-то начинает проясняться, но пока не осознал до конца.

Суть вот:

Стоит запомнить, что ковариантность и контравариантность могут вызывать ошибки времени выполнения. Для их устранения требуется вводить определённые ограничения. Компиляторы, как правило, такие ограничения не вводят.

Если производный тип ковариантен, то для обеспечения безопасности контейнер должен быть read only.

Если производный тип контравариантен, то для обеспечения безопасности контейнер должен быть write only.

---

То есть, List<Device> и List<Keyboard> в общем случае не являются наследниками один другого.

monk ★★★★★ ()

что он может присутствовать в Scala и/или в C#

Сейчас будет пелена нечитаемого текста про Java.

И вот у нас Keyboard - подтип Device

Информация о типах параметризации в момент исполнения недоступна, только в момент компиляции. В JVM это сделано для миграционной совместимости. Это касается Scala и Kotlin.

Чтоб понять почему компилятор такой «тупой» и не позволяет использовать ковариантность в параметризации нужно подключить логику. Пример:

List<Device> items = new ArrayList<Mouse>(); // Ошибка компиляции
return items;
Допустим компилятор проверит, что Mouse является частным случаем Device и скомпилирует, а дальше встречается такой код:
List<Device> items = getItems();
items.add((Device) new Keyboard());
Это валидный код. Явный кастинг здесь для наглядности, Java поддерживает ковариантность и неявное переведение типов. Но в этом случае у нас бы в коллекции из Mouse оказался объект типа Keyboard. Именно для таких случаев придумали правило Producer extends, Customer Super (PECS) и контракты которые декларируются wildcard'ами.

Пример с «Producer extends»:

List<? extends Device> items = new ArrayList<Mouse>();
В этом случае items выступает в роли продюсера - того кто выпускает объекты:
Device device = items.get(1);
Wildcard ? extends T эквивалентен утверждению - объект неизвестного типа, но тип является более специфичным случаем T. В нашем случае декларация ? extends Device утверждает, что коллекция items хранит частные случаи типа Device, это могут быть только Mouse, или только Keybaord, или и то и другое. Именно из-за этой неоднозначности мы не можем вставить объекты любого типа (кроме null):
items.add(new Device()); // Compile-time error

вставляющий «нечто» (any,variant,t) в такой вот «псевдоковариантный» контейнер.

Для того, чтоб имеет возможность вставлять объекты более специфичных типов, мы должны обратиться к другому wildcard'у и правилу «Customer super»:

List<? super Mouse> items = new ArrayList<Device>();
В данном случае <? super Mouse> является как бы прокси интерфейсом к коллекции объектов. Через этот «интерфейс» мы можем передавать объекты типа Mouse или более специфичные, например WirelessMouse.
List<Device> items = getDevices();

List<? super Mouse> maybeMouses = items;
maybeMouses.add(new Mouse()); // OK
maybeMouses.add(new WirelessMouse()); // OK
maybeMouses.add(new Device()); // Compile-time error, у нас контракт

items.add(new Device()); // OK
Но тип элементов в исходной коллекции неизвестен. Правило <? super Mouse> ничего не говорит о том какого типа элементы уже находятся в коллекции. По этой причине:
Object object = maybeMouse.get(1); // OK
Mouse mouse = maybeMouse.get(1); // Compile-time error
В java все типы имеют общего предка - Object. По этой причине единственный гарантированный тип хранимый в коллекции это Object.

Aber ★★★ ()
Ответ на: комментарий от monk

ковариантность и контравариантность могут вызывать ошибки времени выполнения. Для их устранения требуется вводить определённые ограничения. Компиляторы, как правило, такие ограничения не вводят.

Да, я это и имел в виду. Общий смысл: всё плохо.

Вот нашёл кое-что на схожую тему, хотя и не совсем про то. Меня в целом интересуют корректные преобразования параметрических типов.

https://weblog.west-wind.com/posts/2012/Oct/23/Dynamic-Code-for-type-casting-... Насчёт type erasure - я так понял, это костыль для обратной совместимости, а не для того, чтобы гарантировать корректность преобразований параметрических типов.

Видимо, да, нужно со скалой разбираться, а в шарпе, я так понимаю, особо ничего нет.

den73 ★★★★★ ()

Емнип с шарпе List<A> не является «наследником» List<B> если A наследник B.

ya-betmen ★★★★★ ()

Но на самом деле нет. Если мы рассмотрим полную сигнатуру функции «добавить» с учётом динамической типизации, то оказывается, что попытка вставить Mouse в List<Keyboard> имеет определённое поведение, отличающееся от такового для List<Device>. В первом случае будет ошибка времени выполнения (некорректный тип), а во втором мышь вставится в список.

Про Java/Scala/Kotlin , в первом случае будет ошибка времени компиляции. В scala тоже стирание типов, как везде в JVM. Во втором случае ничего делать ненужно, если компилятор дал добро то будет работать как до эры дженериков.

Aber ★★★ ()
Ответ на: комментарий от Aber

О, спасибо, это похоже на то, что надо. А что насчёт http://blog.xapie.nz/2013/12/01/erasure/ - это актуально? Нельзя защититься от этого? Ты нигде явно не написал про это, я могу из неявного «единственный гарантированный тип - это Object» заключить, что защиты от хакерских обманов и последующей ошибки времени выполнения не существует.

P.S. понял, у тебя в примере явно написано, что защиты нет.

den73 ★★★★★ ()
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от den73

Там явный кастинг (T) это ворнинги в коде.

что защиты от хакерских обманов и последующей ошибки времени выполнения не существует.

Если так размышлять то да. Только дженерики не для защиты от хакерских атак придумали :) Просто покажу как раньше (до дженериков) писали на java:

List /* Keybaord */ filteredResults = filter(devices, matchKeyboard);
Keyboard first = (Keyboard) filteredResults.get(0);
...
public boolean processKeyboards(List /* Keybaord */ keyboards)  {
...
}
Aber ★★★ ()
Ответ на: комментарий от Aber

Ну получается, что они добавили сахар, который выглядит как надёжный механизм контроля типов, но который таковым не является из-за того, что внутри информации о типах нет.

Зло возникает здесь:

List<? super Mouse> maybeMouses = items;

Наверное, теоретически можно было бы заставить компилятор это ловить, но если items - это вообще object, к-рый пришёл параметром, то проверить корректность невозможно ни в compile-time, ни в runtime. Т.е. это нехилая дыра в надёжности языка.

Попробую понять, как обстоит дело в C# - и обстоит оно вот так:

https://stackoverflow.com/questions/31693/what-are-the-differences-between-ge...

В C# в рантайме известно, что List<Mouse> хранит экземпляры Mouse, и это правильно. А в Java просто кривое решение из-за совместимости, которое не стоит рассматривать как образец для подражания.

den73 ★★★★★ ()
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от Aber

Честно говоря, я был о Scala лучшего мнения.

den73 ★★★★★ ()
Ответ на: комментарий от den73

Они делают какой-то workaround чтоб преодолеть стирание типов, но это лучше у спецов по scala спросить.

Ты нужные тебе языки в тегах выстави, тогда заинтересованные лица получать нотификацию о топике.

Aber ★★★ ()
Последнее исправление: Aber (всего исправлений: 1)
Ответ на: комментарий от Aber

Посмотрел на их обходные пути, правда у меня нет проблемы удаления типов, поэтому не особо интересно. Минимизируая количество тегов, я регулирую объём ругани на рекламу Яра через тег :)

В общем, похоже, решение задачи таково, что если нужен массив именно мышей, то и передаём массив именно мышей. Если можно такой или массив чего-то более общего, то подойдёт PECS (если абстрагируемся от дыр в защите).

den73 ★★★★★ ()
Ответ на: комментарий от yoghurt

Да я тут вообще CL не упоминаю. В шарпе и Яве есть базовый тип Object, к-рый вполне себе динамическая типизация.

den73 ★★★★★ ()

все классы должны быть явно/неявно унаследованы от Object?

bvn13 ★★★★★ ()

а во втором мышь вставится в список.

Т.е. здесь нет ковариантности.

Я не понял хода мысли. ковариантности нет потому что мышь вставится в список устройств? Так это неверное утверждение, в список устройств вставляется любое устройство, это же список устройств, а не список клавиатур. Тут нет никакого противоречия.

Согласно принципу Лисков, экземпляр подтипа должен вести себя так же, как экземпляр супертипа.

Поддерживать интерфейс, а не вести себя так же

onceagain2017 ()

По-человечески ко- и контравариантность сделаны в шарпе. Чтоб не мучать людей терминологией, символьной абракадаброй и правилами использования в шарпе просто взяли и заюзали слова in и out. class Foo<in A, out B>{...} И в общем всё ясно даже тому, кто про вариантность не слышал, объекты типа A класс только получает, поэтому Foo<base, X> можно использовать вместо Foo<derived, X> (контравариантность). Объекты типа B класс только выдаёт, Foo<X, derived> можно использовать вместо Foo<X, base>, значит класс Foo ковариантен своему второму параметру.

khrundel ★★ ()
Ответ на: комментарий от den73

Тем не менее какой в этом прок с практической точки зрения?

Имея подобную структуру (могу напутать в синтаксисе шарпа)

public abstract class A<T> where T : B{}
public class B {}
public class C0: B {}
public class C1: B {}
public class D0 : A<C0> {}
public class D1 : A<C1> {}
Я не смогу использовать обобщенный вариант A<B> в тех случаях где мне нужны либо D0 либо D1. И вместо удобных
C0 param0;
C1 param1;
придется пользоваться
B param0;
B param1;

ya-betmen ★★★★★ ()
Последнее исправление: ya-betmen (всего исправлений: 2)
Ответ на: комментарий от khrundel

Тогда вопрос на засыпку: ограничиваются ли этим все случаи, когда ковариантность и контравариантность полезны (я ответа сам не знаю)?

den73 ★★★★★ ()
Ответ на: комментарий от den73

Каким случаем? Разделение на «только на вход» и «только на выход» - это не частный случай, а обязательное требование вариантности, иначе Лискова обидится. А частные случаи, это IEnumerable<out T> и Func<in TParam,... out TResult>, может где-то ещё тоже используется.

khrundel ★★ ()
Последнее исправление: khrundel (всего исправлений: 2)
Ответ на: комментарий от ya-betmen

Я не смогу использовать обобщенный вариант A<B> в тех случаях где мне нужны либо D0 либо D1.

Ты имеешь в виду, в методах D0 и в C#? А в Яве сможешь? Независимо от этого, преимущество C# в том, что можно гарантировать, что в список мышей не будет вставлена клавиатура, а в Яве такой гарантии нет.

И при этом всё же можно в уместных случаях передать список мышей вместо списка устройств, или наоборот.

den73 ★★★★★ ()
Ответ на: комментарий от khrundel

Случаем «только на вход» и «только на выход». Пример в порядке мозгового штурма:

Мышь Омышить(object X) {
  return new Мышь();
}

ПреобразоватьКаждыйЭлементФункцией(List<Т>,Омышить)
Здесь Т может быть вообще любым типом, т.к. функция всегда возвращает Мышь. При этом контейнер и пишется, и читается.

den73 ★★★★★ ()
Ответ на: комментарий от den73

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

Да, но практически от этого какая польза?

ya-betmen ★★★★★ ()
Ответ на: комментарий от den73

Т.е. это не ковариантность, не контравариантость, а пофигвариантность.

den73 ★★★★★ ()
Ответ на: комментарий от ya-betmen

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

den73 ★★★★★ ()
Ответ на: комментарий от den73

Я попробовал поискать по ключам, ничего не нашел. Ты можешь процитировать конкретный кусок?

onceagain2017 ()
Ответ на: комментарий от den73

Когда в список мышей можно вставить клавиатуру без ошибки во время компиляции

Без явного каста не вставишь.

и выполнения

Откуда она там появится во время выполнения?

Или тебя интересует голая теория?

ya-betmen ★★★★★ ()
Последнее исправление: ya-betmen (всего исправлений: 1)
Ответ на: комментарий от onceagain2017

Пункт 3.3, type hierarchy:

The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra. What is wanted here is something like the following substitution property [6]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.

Т.е речь не идёт об интерфейсах, а идёт именно о поведении. Как мы его определим - зависит от нас, т.е. например, включаем ли мы в «поведение» рефлексию или исключения. При этом получится разное определение подтипа. Если например, мы включим рефлексию и какое-нибудь ClassName (не знаю, как оно называется), то может оказаться, что подтипов в языках программирования вообще нет :)

den73 ★★★★★ ()
Ответ на: комментарий от ya-betmen

Откуда она там появится во время выполнения?

В шарпе она там есть. Берётся оттуда, что компилятор её туда засовывает. В Scala есть костыль - TypeTag/ClassTag.

Без явного каста не вставишь.

Для меня это недостаточная защита.

den73 ★★★★★ ()
Ответ на: комментарий от den73

В шарпе она там есть. Берётся оттуда, что компилятор её туда засовывает. В Scala есть костыль - TypeTag/ClassTag.

Я про клаву в списке мышей. Если её туда нельзя вставить то как она там появится?

Для меня это недостаточная защита.

Хм. Ты хочешь запретить людям хотеть странного?

ya-betmen ★★★★★ ()
Ответ на: комментарий от den73

provide

Ну, поддерживать — это не значит быть одинаковым.

В данном контексте, я думаю, это следует понимать как поддержка протокола.

Если, скажем, есть тип «самолет», и есть его подтипы «турбовинтовой» и «турбореактивный», а клиент использует тип самолет, и ему нет дела до деталей реализации полета, он должен просто слать сообщение «самолет лети». Его устроит даже аэроплан.

onceagain2017 ()
Ответ на: комментарий от den73

Здесь Т может быть вообще любым типом, т.к. функция всегда возвращает Мышь. При этом контейнер и пишется, и читается.

Ну, наверное не любым, string, например не может, ТрёхКнопочнаяМышь тоже не может. Может быть мышью и её предком, вплоть до object.

Как такое получилось? Неужели List<T> перестал быть инвариантным?

Конечно нет, просто хитрожопый den73 всех обманул, незаметно подсунул вариантность в другом элементе уравнения. Тип функции Омышить можно описать как Func<object, Мышь>, она ковариантна типу результата и контравариантна типу параметра, следовательно, может быть неявно кастнута к любому типу Func<T1, T2>, где T1 - это object или его потомок, а T2 - мышь или её предок.

ПреобразоватьКаждыйЭлементФункцией(List<object>, (Func<object, object>)Омышить); // OK
ПреобразоватьКаждыйЭлементФункцией(List<устройство>, (Func<Устройство, Устройство>)Омышить); //OK
ПреобразоватьКаждыйЭлементФункцией(List<мышь>, (Func<Мышь, Мышь>)Омышить); //OK
ПреобразоватьКаждыйЭлементФункцией(List<ТрёхКнопочнаяМышь>, (Func<ТрёхКнопочнаяМышь, ТрёхКнопочнаяМышь>)Омышить); //ОШИБКА: Омышить нельзя преобразовать, так как ТрёхКнопочнаяМышь - это наследник класса Мышь, ковариантность не позволяет от предка к наследнику.

khrundel ★★ ()
Ответ на: комментарий от den73

я предложил вариант реализации. вопрос был: предложенное не подходит?

bvn13 ★★★★★ ()
Ответ на: комментарий от bvn13

Да они вроде и так унаследованы. Я имею в виду, это должно быть что-то в языке, которое говорило бы, что в этом случае нам неважно , порождается ли ошибка для других типов. По сути in и out - это похожие вещи, главное, что ко и контрвариантность зависят не от самих типов, а от конкретного случая их применения (я это понял только по ходу чтения этой темы). Их можно не включать, если они не нужны. Т.е. вообще речь оказывается идёт не о «ковариантности параметрического типа контейнера типу его элемента», а о «ковариантности параметрического типа контейнера типу его элемента в данном конкретном методе». Что само по себе повод задуматься.

den73 ★★★★★ ()
Ответ на: комментарий от khrundel

Может быть любым, т.к. параметр игнорируется, а вместо него возвращается новая мышь. Можно придумать и более содержательный пример (например, можно превратить любой объект в строку, напечатав его).

Тип функции Омышить можно описать как Func<object, Мышь>, она ковариантна типу результата и контравариантна типу параметра

Ей наплевать на тип параметра. Разве это контравариантность?

ПреобразоватьКаждыйЭлементФункцией(List<Устройство>, (Func<Object, Устройство>)Омышить); //OK
Как назвать такой случай?

А вообще я не пытался никого обмануть, я просто пытаюсь «покрутить» это понятие со всех сторон и понять, есть ли у него другие области применения, кроме как «подсунуть другой контейнер вместо данного в качестве аргумента или делегата». Ведь в справке по языку этого не напишут.

den73 ★★★★★ ()
Ответ на: комментарий от ya-betmen

Я про клаву в списке мышей. Если её туда нельзя вставить то как она там появится?

В начале был приведён пример, как это случится. Да, он с явным приведением типа.

Хм. Ты хочешь запретить людям хотеть странного?

Хотеть странного можно, но стандартные контейнеры должны быть безопасными. Написано - контейнер мышей, значит это должен быть контейнер только для мышей, чтобы в него _никак_ нельзя было вставить слона. Для странного пусть пишут своё.

den73 ★★★★★ ()
Ответ на: комментарий от den73

Может быть любым, т.к. параметр игнорируется, а вместо него возвращается новая мышь. Можно придумать и более содержательный пример (например, можно превратить любой объект в строку, напечатав его).

Абсолютно неважно. Для языка важен контракт фукнции, а не её внутренности, в вашем случае она принимает object, как всеобщий предок. Могла бы принимать, например, Устройство, тогда вся эта конструкция работала бы для любых типов в линии наследования от Устройства до Мышь.

Как назвать такой случай?

Обычное неявное приведение функции по вариантности к другой?

У вас, я так понимаю, должен быть дженерик

void ПреобразоватьКаждыйЭлементФункцией<T>(IList<T> list, Func<T,T> mappingFunc);
Этот дженерик инвариантен параметру T. Массив тоже инвариантен T. Всех спасает функция Омышить, она может быть приведена к Func<T,T> в случае если T равен или наследник object и если T равен или предок Мышь, вот компилятор её и приводит.

khrundel ★★ ()
Ответ на: комментарий от khrundel

Может, я что-то упускаю, но мне кажется, что инвариантность вообще нет смысла обсуждать - это просто «отсутствие ковариантности и контравариантности» - она доступна по умолчанию сразу, как только возникают параметрические типы. Верно?

Насчёт сигнатуры преобразователя функцией, если бы можно было записать, то он должен быть таким:

void ПреобразоватьКаждыйЭлементФункцией<Ts,Td>(
  IList<Ts U Td> list, 
  Func<Ts in,Td out> mappingFunc
);
Здесь Ts U Td - это объединение типов, т.е. ему удовлетворяет объект, удовлетворяющий хотя бы одному из Ts и Td. В С#, возможно, такого типа не бывает, а в лиспе он бывает.

Соответственно вопрос, что можно сказать о вариантности Ts и Td - в самом ПреобразоватьКаждыйЭлементФункции ?

den73 ★★★★★ ()
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от Minona

Оригинальное определение LSP не содержит упоминания о поведении.

Содержит, содержит.

elyadow ()

А теперь вопрос: что используется в ковариантности в языках, позволяющих скомпилировать код, вставляющий «нечто» (any,variant,t) в такой вот «псевдоковариантный» контейнер.

Мне кажется, должен быть какой-то костыль для этой ситуации,

cast den73

язык Eiffel: опорный элемент, var1: like var2, или obj1: like Current внутри класса (=тип self)

anonymous ()
Ответ на: комментарий от yoghurt

полиглот

интересная сборка, спасибо.

надо же, squeak на русском :-)

и содержимое образа радует: OMeta SmaCC

SmaCC

We have used SmaCC to write custom refactoring and transformation tools for several languages including Java, C#, and Delphi. These tools range from small scale refactorings to large scale migration projects. For example, we have used SmaCC to migrate a 1.5 million line Delphi program to C#.

тут

парсеры Delphi, C, C#, Java, IDL, Javascript, Smalltalk, Swift

спасибо, очень интересно

anonymous ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.