Ковариантность и контравариантность php
Представляем PHP 7.4: Производительность, Возможности, Устаревший Функционал
PHP постоянно развивается, и только что мир увидело их последнее обновление — PHP 7.4. Производительность и скорость продолжают расти, в чём мы убедились ещё в предыдущих релизах PHP 7. Предварительная загрузка — одно из самых ярких обновлений. Эта возможность позволяет значительно ускорить выполнение скрипта и делает код чище и быстрее благодаря уменьшённому количеству строк кода.
PHP является важным элементом всемирной паутины и используется более чем на 79% всех сайтов. Известные сайты, такие как Facebook, Википедия, WordPress и многие другие используют PHP.
Мы можем увидеть что скорость веб-сайтов WordPress увеличивается вдвое, если сравним сайты, работающие на PHP 5 и 7. От последней версии больше всего выиграют сайты на WordPress.
В настоящее время PHP используют около 40 млн. сайтов. Этот график иллюстрирует производительность сайтов, которые активно используют PHP. В целом, PHP 7.4 выглядит довольно неплохо по сравнению с PHP 7.3: лучшая производительность, к которой добавились возможности FFI, функция предварительной загрузки и другие улучшения.
На следующем графике сравниваются общие результаты теста производительности 7.4 и более старых версий PHP.
PHP 7.4: Что Нового?
Постоянные ежегодные обновления PHP7 начали выходить с 2016 года: различные новые функции, возможность писать более чистый код, чтобы сделать язык более надёжным и удобным, если вы запускаете его на своём сайте и т.д. Давайте взглянем на некоторые модификации, которые появились с выходом версии PHP 7.4.
Полный список изменений можно посмотреть в их ченджлоге.
Предварительная Загрузка
Предварительная загрузка — позволяет загружать фреймворки и библиотеки в OPCache. Это действительно удобная функция, поскольку при каждом использовании файлов или библиотек необходимо загружать и связывать файлы по каждому запросу. Загружает файлы PHP и хранит их в памяти, чтобы они были готовы к любым поступающим запросам — вот, что делает эта фича!
Предварительная загрузка контролируется единственной новой директивой php.ini: opcache.preload. Используя эту директиву, мы можем указать один файл PHP — тот, который будет выполнять задачу предварительной загрузки. После загрузки этот файл полностью выполняется и может предварительно загружать другие файлы, либо включая их, либо используя функцию opcache_compile_file ().
Как уже было упомянуто выше, предварительно загруженные файлы будут закешированны в памяти opcache на неопределённый срок. Модификация их соответствующих исходных файлов не будет иметь никакого эффекта без перезапуска другого сервера.
Поэтому, если вам захочется снова использовать эти предварительно загруженные файлы, они будут доступны для любых предстоящих запросов.
Оператор Spread Вместо array_merge
PHP начал поддерживать распаковку аргументов (Оператор Spread), начиная с версии PHP 5.6. Теперь, с выходом 7.4, мы можем использовать оператор Spread в массивах. Синтаксис для распаковки массивов и Traversables в списки аргументов называется распаковкой аргументов. И чтобы это произошло, вам нужно всего лишь добавить три точки (…) в начале.
Взгляните на этот пример:
Теперь мы также можем расширить массив другим массивом с любого места, используя синтаксис оператора Spread.
Более длинный пример:
Кроме того, вы можете использовать его в функции. Как в этом примере:
Также вы можете распаковывать массивы и генераторы, которые возвращаются функцией, прямо в новый массив:
PHP 7.4 выводит следующий массив:
Используя это выражение массива, операторы спреда должны иметь лучшую производительность по сравнению с 7.3 array_merge. Это связано с тем, что оператор Spread является языковой структурой, а array_merge — функцией. Другая причина заключается том, что оператор спреда поддерживает реализацию объектов Traversable, в то время как array_merge поддерживает только массивы.
Ещё одна потрясающая и действительно удобная функция, которая появилась в 7.4 — это удаление индексированных массивов array_merge. Строковые ключи не поддерживаются. Больше никаких смещений индекса!
Ещё одним преимуществом PHP 7.4 является функциии-генераторы. Разница между генератором и обычной функцией заключается в том, что функция-генератор будет перебирать столько значений, сколько необходимо, вместо того, чтобы возвратить одно значение. Итак, любая функция, содержащая ключевое слово yield, является функцией-генератором:
Слабые ссылки
В PHP 7.4 есть класс WeakReference, который позволяет сохранить ссылку на объект, не препятствующую его уничтожению. Слабые ссылки полезны для реализации кеш-подобных структур.
Не путайте WeakReference с классом WeakRef или расширением Weakref.
Контравариантные Параметры и Ковариантные Возвраты
На данный момент PHP в основном использует инвариантные типы параметров и возвращаемого значения. Это подразумевает, что подтип параметра или тип возвращаемого значения также должны быть типа X, если метод имеет параметр или тип возвращаемого значения типа X.
В PHP 7.4 появляется ковариантность (порядок от конкретного к общему) и контравариантность (обратный порядок) для типов возвращаемых данных и параметров.
Давайте посмотрим на оба примера:
Ковариантный тип возвращаемых данных:
Контравариантный тип параметра:
Типизированные Свойства 2.0
Функция контроля типов (type hints) появилась ещё в PHP 5. Она позволяет указать тип переменной, которая должна быть передана функции или методу класса.
Начиная с PHP 7.2 стало возможным использовать type hints с типом данных объекта, что вселило надежду получить ещё больше возможностей.
В PHP 7.4 стаёт возможной поддержка следующего списка типов:
Родительский тип может использоваться в классах, и ему не нужно иметь родительскую константу с параметром и типом возвращаемого значения.
Поддерживаются все типы, кроме void и callable. Callable был удалён, потому что его поведение зависело от контекста; а void оказался недостаточно полезным, кроме того свою роль сыграла неясная семантика типа.
Давайте взглянем на пример ниже.
В PHP 7.4 классы выглядят намного проще:
Поддерживаемые типы PHP 7.4:
Стрелочные Функции 2.0
Не секрет, что анонимные функции в PHP слишком громоздкие и продолжительные, даже если речь идёт о выполнении самых простых действий. Поэтому из JavaScript были позаимствованы стрелочные функции, или короткие замыкания. Их задача оптимизировать код PHP, улучшить его читаемость.
Анонимные функции усложняют чтение кода с второстепенной функциональностью.
Вот пример анонимной функции в PHP 7.3:
А это легко читаемая стрелочная функция в PHP 7.4:
Следовательно, вот насколько простыми являются стрелочные функции:
Вы видите две функции $fn1 и $fn2. Код выглядит иначе, но функции передают идентичный результат.
Это также наверняка будет работать с вложенными стрелочными функциями:
Синтаксис стрелочных функций позволяет нам использовать переменные, значения по умолчанию, параметры и типы возвращаемых данных, а также передавать и возвращать по ссылке и т. Д. Новый синтаксис является значительным улучшением языка, поскольку он позволяет нам создавать более читабельный и понятный код.
Также стоит отметить, что стрелочные функции имеют самый низкий приоритет:
Устаревшие Функции
На данный момент приоритет операторов “.”, “+” и “-” одинаков. Любая комбинация этих операторов будет решена слева направо.
Взгляните на пример кода PHP 7.3:
В PHP 7.4 сложения и вычитания всегда выполняются перед стрингой, поскольку “+” и “-” имеют приоритет над “.”:
Левоассоциативный Тернарный Оператор
В отличие от большинства других языков, где тернарный оператор обрабатывается справа на лево, в PHP это происходит наоборот. Такое поведение часто сбивает разработчиков столку, поэтому было предложено использовать круглые скобки, вместо левой ассоциативности.
Давайте посмотрим на следующий пример:
В большинстве других языков это читается так:
В то время как в PHP, это читается:
Поскольку часто это оказывается не тем, чем планировалось, в результате мы получим ошибки.
Выводы
PHP 7.4 представил новые удобные функции для каждого PHP-разработчика.
Сайты WordPress и их пользователи определённо выиграют от этих улучшений. Сокращение использования памяти и быстрое время выполнения — это лишь некоторые преимущества PHP 7.4 перед старыми релизами.
PHP 7.4, безусловно, повысит как скорость, так и качество вашего рабочего процесса благодаря представленным стрелочным функциям, объявлению типов свойств первого класса и контролю типов, а также намного лучшей скорости.
Новое в PHP 7.4
Новая версия PHP хоть и является минорной, но уже несёт множество новых, без преувеличения, крутых возможностей как для синтаксиса языка, так и для его производительности. Список новшеств не окончательный, но основные изменения уже внесены и приняты. Релиз планируется на декабрь 2019 года.
Ключевые изменения грядущей версии:
Стрелочные функции (RFC)
Стрелочные функции позволяют делать более короткую запись анонимных функций:
Некоторые особенности принятой реализации стрелочных функций:
Типизированные свойства (RFC)
Ура! Свойства класса теперь смогут иметь type hint. Это очень долгожданное со времён PHP 7 изменение в направлении более строгой типизации языка. Теперь у нас есть все основные возможности для строгой типизации. Для типизации доступны все типы, за исключением void и callable.
Если хотите чтобы я более подробно рассмотрел новые возможности типизации свойств объектов, отметьтесь в комментариях, и я напишу отдельную статью про них!
Присваивающий оператор объединения с null (RFC)
Вместо такой длинной записи:
Теперь можно будет написать так:
Оператор распаковки в массивах (RFC)
Теперь можно использовать оператор распаковки в массивах:
Обратите внимание, что это работает только с неассоциативными массивами.
Интерфейс внешних функций (RFC)
Интерфейс внешних функций (FFI) позволяет писать код на C непосредственно в PHP-коде. Это означает, что расширения PHP могут быть написаны на чистом PHP.
Следует отметить, что это сложная тема. Вам всё ещё нужно знание C, чтобы правильно использовать эти возможности.
Предзагрузка (RFC)
Предварительная загрузка — потрясающее дополнение к ядру PHP, которое должно привести к некоторым значительным улучшениям производительности.
Например, если вы используете фреймворк, его файлы должны быть загружены и перекомпилированы по каждому запросу. При использовании opcache — при первом участии этих файлов в процессе обработки запроса и далее при каждом успешной проверке на их изменения. Предварительная загрузка позволяет серверу загружать указанные PHP-файлы в память при запуске и иметь их постоянно доступными для всех последующих запросов, без осуществления дополнительных проверок на изменения файлов.
Повышение производительности происходит «небесплатно» — если предварительно загруженные файлы изменяются, сервер должен быть перезапущен.
Ковариантность/контравариантность в сигнатурах унаследованных методов (RFC)
В настоящее время PHP имеет в основном инвариантные типы параметров и инвариантные возвращаемые типы. Данное изменение позволяет изменять тип параметра на один из его супертипов. В свою очередь возвращаемый тип можно заменить на его подтип. Таким образом, данное изменение позволит более строго следовать принципу подстановки Барбары Лисков.
Пример использования ковариантного возвращаемого типа:
и контравариантного аргумента:
Пользовательская сериализация объектов (RFC)
Приоритет операций при конкатенации (RFC)
Если бы вы написали что-нибудь подобное:
Сейчас PHP интерпретировал бы это как:
PHP 8 будет интерпретировать это иначе:
PHP 7.4 добавляет предупреждение об устаревании при обнаружении выражения, содержащего «.» перед «+ » или «-» и неокружённого при этом скобками.
Поддержка исключений в __toString (RFC)
Рефлексия для ссылок (RFC)
Добавлен метод mb_str_split (RFC)
Всегда доступное расширение ext-hash (RFC)
Это расширение теперь постоянно доступно во всех установках PHP.
PEAR не включен по умолчанию (EXTERNALS)
PEAR больше не поддерживается активно, core-команда решила удалить его из установки по умолчанию с PHP 7.4.
Реестр алгоритмов хэширования паролей (RFC)
Слабые ссылки (RFC)
Слабые ссылки позволяют сохранить ссылку на объект, которая не препятствует уничтожению этого объекта. Например, они полезны для реализации кэш-подобных структур.
Разделитель числовых литералов (RFC)
Отсутствие визуальных разделителей в группах цифр увеличивало время чтения и отладки кода, и могло привести к непреднамеренным ошибкам. Теперь добавлена поддержка символа подчёркивания в числовых литералах для визуального разделения групп цифр.
Короткие открывающие теги объявлены устаревшими (RFC)
Короткий открывающий тег устарел и будет удалён в PHP 8. Короткий тег не пострадал.
Левоассоциативный тернарный оператор объявлен устаревшим (RFC)
Тернарный оператор имеет некоторые странные причуды в PHP. Этот RFC объявляет устаревшими вложенные тернарные операторы.
В PHP 8 такая запись приведёт к ошибке уровня компиляции.
Обратно несовместимые изменения (UPGRADING)
Вот некоторые из самых важных на данный момент обратно несовместимых изменений:
Вариантность в программировании
До сих пор не можете спать, пытаясь осмыслить понятия ковариантности и контравариантности? Чувствуете, как они дышат вам в спину, но когда оборачиваетесь ничего не находите? Есть решение!
Меня зовут Никита, и сегодня мы попытаемся заставить механизм в голове работать корректно. Вас ожидает максимально доступное рассмотрение темы вариантности в примерах. Добро пожаловать под кат.
Брифинг
Вариантность в данном посте разбирается безотносительно к какому-либо языку программирования. Примеры в разделе практики написаны на псевдоязыке (он чудом оказался похож на C#) и поэтому не обязаны компилироваться вашим любимым компилятором. Приступим.
Хитрости терминологии
В документации, технической литературе и других источниках вы могли встречаться с различными названиями для явлений вариантности. Больше не стоит пугаться и путаться.
Термины ковариантность и ковариация эквивалентны (по крайней мере в программировании). Более того, термины контравариантность и контравариация также эквивалентны. Так, например, термины ковариантность и контравариантность используется в Википедии и у Троелсена (в переводе). А термины ковариация и контравариация встречаются, например, на MSDN и у Скита (в переводе).
В английском языке всё проще — covariance и contravariance.
Теория
Вариантность — перенос наследования исходных типов на производные от них типы. Под производными типами понимаются контейнеры, делегаты, обобщения, а не типы, связанные отношениями «предок-потомок». Различными видами вариантности являются ковариантность, контравариантность и инвариантность.
Ковариантность — перенос наследования исходных типов на производные от них типы в прямом порядке.
Контравариантность — перенос наследования исходных типов на производные от них типы в обратном порядке.
Инвариантность — ситуация, когда наследование исходных типов не переносится на производные.
Если у производных типов наблюдается ковариантность, говорят, что они ковариантны исходному типу. Если у производных типов наблюдается контравариантность, говорят, что они контравариантны исходному типу. Если у производных типов не наблюдается ни того, ни другого, говорят, что они инвариантны.
Вот и всё, что нужно знать. Конечно, тем кто первый раз сталкивается с вариантностью, трудно вникнуть. Поэтому рассмотрим конкретные примеры.
Практика
Для чего всё это?
Вся суть вариантности состоит в использовании в производных типах преимуществ наследования. Известно, что если два типа связаны отношением «предок-потомок», то объект потомка может храниться в переменной типа предка. На практике это значит, что мы можем использовать для каких-либо операций объекты потомка вместо объектов предка. Тем самым, можно писать более гибкий и короткий код для выполнения действий поддерживаемых разными потомками с общим предком.
Исходная иерархия и производные типы
Для начала опишем иерархию типов, которой будем оперировать. Вверху иерархии у нас находится Device (устройство), потомками которого являются Mouse (мышь), Keyboard (клавиатура). У Mouse в свою очередь тоже есть потомки — WiredMouse (проводная мышь), WirelessMouse (беспроводная мышь).
Все любят контейнеры. На их примере наиболее просто объяснить, что подразумевается под производными типами. Если говорить о списках как производных типах, то для типа Device производным будет
List (список устройств). Аналогично, для типа Keyboard производным будет List (список клавиатур). Думаю, если и были сомнения, то теперь их нет.
Классическая ковариантность
Ковариантность также легче изучать на примере контейнеров. Для этого выделим часть иерархии (ветвь) — Keyboard : Device (клавиатура является устройством, клавиатура частный случай устройства). Опять возьмём списки и построим ковариантную производную ветвь — List : List (список клавиатур является частным случаем списка устройств). Как видим, наследование передалось в прямом порядке.
Рассмотрим пример кода. Есть функция, которая принимает список устройств List и совершает над ними какие-то манипуляции. Как вы уже догадались, в эту функцию можно передать список клавиатур List :
Классическая контравариантность
Каноническим для изучения контравариантности является рассмотрение её на основе делегатов. Допустим, у нас есть обобщённый делегат:
Немного инвариантности
Если производные типы инвариантны к исходным типам, то для ветви Keyboard : Device не образуется ни ковариантной ( List : List ), ни контравариантной ( Action : Action ) ветви. Это значит, что нет никакой связи между производными типами. Как видим, наследование не переносится.
А что если?
Неочевидная ковариантность
Неочевидная контравариантность
Сакральный смысл
Рассмотренные выше экзотические виды вариантности имеют, разве что, академическую ценность. Сложно придумать реальную задачу, которая легче решается при наличии такого рода возможностей. Стоит запомнить, что ковариантность и контравариантность могут вызывать ошибки времени выполнения. Для их устранения требуется вводить определённые ограничения. Компиляторы, как правило, такие ограничения не вводят.
Безопасность для контейнеров
Двойные стандарты для делегатов
Разумным для делегатов является ковариантность для выходного значения и контравариантность для входных параметров (исключая передачу по ссылке). В случае соблюдения данных условий ошибок времени выполнения не возникает.
Дебрифинг
Представленных примеров достаточно для понимания принципов работы вариантности. Данные о её поддержке разными типами вашего любимого языка ищите в соответствующей спецификации. Если что-то пошло не так — закройте глаза, выдохните и выпейте чай. После этого попытайтесь снова. Спасибо за внимание.
Возможно более правильным определением вариантности является предложенное Эриком Липпертом. Спасибо Alex_sik за ссылку на статью.
Совместимость присваивания, assignment compatibility — это возможность присвоить значение более частного типа совместимой переменной более общего типа.
Вариантность — это сохранение совместимости присваивания исходных типов у производных типов.
Ковариантность — это сохранение совместимости присваивания исходных типов у производных в прямом порядке.
Контравариантность — это сохранение совместимости присваивания исходных типов у производных в обратном порядке.
Теория программирования: Вариантность
Здравствуйте, меня зовут Дмитрий Карловский и я… хочу поведать вам о фундаментальной особенности систем типов, которую зачастую или вообще не понимают или понимают не правильно через призму реализации конкретного языка, который ввиду эволюционного развития имеет много атавизмов. Поэтому, даже если вы думаете, что знаете, что такое «вариантность», постарайтесь взглянуть на проблематику свежим взглядом. Начнём мы с самых основ, так что даже новичок всё поймёт. А продолжим без воды, чтобы даже профи было полезно для структурирования своих знаний. Примеры кода будут на псевдоязыке похожем на TypeScript. Потом будут разобраны подходы уже нескольких реальных языков. А если же вы разрабатываете свой язык, то данная статья поможет вам не наступить на чужие грабли.
Аргументы и параметры
Параметр — это то, что мы принимаем. Описывая тип параметра мы задаём ограничение на множество типов, которые можно нам передать. Несколько примеров :
Аргумент — это то, что мы передаём. В момент передачи аргумент всегда имеет какой-то конкретный тип. Тем не менее, при статическом анализе конкретный тип может быть не известен из-за чего компилятор оперирует опять же ограничениями на тип. Несколько примеров:
Подтипы
Типы могут могут образовывать иерархию. Подтип — это частный случай надтипа. Подтип может образовываться путём сужения множества возможных значений надтипа. Например, тип Natural является подтипом Integer и Positive. И все трое одновременно являются подтипами Real. А тип Prime является подтипом всех вышеперечисленных. В то же время типы Positive и Integer являются пересекающимися, но ни один из них не является сужением другого.
Другой способ образовать подтип — расширить его, объединив с другим ортогональным ему типом. Например, есть «цветная фигура» имеющая свойство «цвет», а есть «квадрат» имеющий свойство «высота». Объединив эти типы мы получим «цветной квадрат». А добавив «круг» с его «радиусом» можем получить «цветной цилиндр».
Иерархии
Для дальнейшего повествования нам понадобится небольшая иерархия животных и аналогичная ей иерархия клеток.
Всё, что ниже на рисунке является сужением типа выше. Клетка с питомцем может содержать лишь домашних животных, но не диких. Клетка с собакой может содержать лишь собак.
Ковариантность
Самое простое и понятное — это ограничение надтипа или ковариантность. В следующем примере параметр функции ковариантен указанному для него типу. То есть функция может принимать как сам этот тип, так и любой его подтип, но не может принимать надтипы или иные типы.
Так как мы ничего не меняем в клетке, то спокойно можем передавать функции клетку с кошкой, так как она — не более, чем частный случай клетки с питомцем.
Контравариантность
Чуть сложнее понять ограничение подтипа или контравариантность. В следующем примере параметр функции контравариантен указанному для него типу. То есть функция может принимать как сам этот тип, так и любой его надтип, но не может принимать подтипы или иные типы.
Мы не можем передать клетку с кошкой, так как функция может засунуть туда собаку, что не допустимо. А вот клетку с любым животным можно смело передавать, так как и кошку и собаку вполне можно туда помещать.
Инвариантность
Ограничение подтипа и надтипа могут быть одновременно. Такой случай называется инвариантностью. В следующем примере параметр функции инвариантен указанному для него типу. То есть функция может принимать только указанный тип и никакой больше.
Бивариантность
Нельзя не упомянуть и экзотическое отсутствие ограничений — бивариантность. В следующем примере функция может принять любой тип являющийся надтипом или подтипом.
В неё можно передать клетку с животным. Тогда она проверит, что в клетке находится питомец, иначе положит внутрь случайного питомца. А можно передать и, например, клетку с кошкой, тогда она просто ничего не сделает.
Обобщения
Некоторые считают, что вариантность как-то связана с обобщениями. Зачастую потому, что про вариантность часто объясняют на примере обобщённых контейнеров. Однако, во всём повествовании у нас до сих пор не было ни единого обобщения — сплошь конкретные классы:
Сделано это было, чтобы показать, что проблемы вариантности никак с обобщениями не связаны. Обобщения нужны лишь, чтобы уменьшить копипасту. Например, код выше можно переписать через простое обобщение:
И теперь можно создавать экземпляры любых клеток:
Декларация ограничений
Обратите внимание, что сигнатуры у всех четырёх ранее приведённых функций совершенно одинаковые:
То есть такое описание принимаемых параметров функции не обладает полнотой — по нему нельзя сказать что в функцию можно передавать. Ну разве что однозначно видно, что клетку с лисой передавать в неё точно не стоит.
Поэтому в современных языках есть средства для явного указания какие у параметра ограничения на типы. Например, модификаторы in и out в C#:
К сожалению, в C# для каждого варианта модификаторов необходимо заводить по отдельному интерфейсу. Кроме того, насколько я понял, бивариантность в C# вообще невыразима.
Выходные параметры
Функции могут не только принимать, но и возвращать значения. В общем случае, возвращаемое значение может быть и не одно. В качестве примера возьмём функцию принимающую клетку с питомцем и возвращающую двух питомцев.
Такая функция эквивалентна функции, принимающей помимо одного входного параметра, ещё и два выходных.
Внешний код выделяет на стеке дополнительную память, чтобы функция положила в неё всё, что хочет вернуть. А по завершении, вызывающий код уже сможет воспользоваться этими контейнерами в своих целях.
Из эквивалентности этих двух функций следует, что возвращаемые функцией значения в отличие от параметров всегда контравариантны указанному выходному типу. Ибо функция может в них писать, но не может из них читать.
Методы объектов
Методы объектов — это такие функции, которые принимают дополнительный указатель на объект в качестве неявного параметра. То есть следующие две функции эквивалентны.
Однако, важно отметить, что метод, в отличие от обычной функции, является также и членом класса, что является расширением типа. Приводит это к тому, что появляется дополнительное ограничение надтипа, для функций, вызывающих этот метод:
Мы не можем передать в неё такой надтип, где метод pushPet ещё не определён. Это похоже на случай с инвариантностью тем, что есть ограничение и снизу и сверху. Однако, место определения метода pushPet может быть выше по иерархии. И именно там будет ограничение надтипа.
Принцип Подстановки Барбары Лисков (LSP)
Многие считают, что отношение надтип-подтип определяется не исходя из упомянутых ранее способов сужения и расширения типа, а возможностью подставить подтип в любое место использования надтипа. По всей видимости, причина этого заблуждения именно в LSP. Однако, давайте прочитаем определение этого принципа внимательно, обратив внимание, что первично, а что вторично:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом и не нарушая корректность работы программы.
Для неизменяемых (в том числе не ссылающихся на изменяемые) объектов этот принцип выполняется автоматически, так как неоткуда взяться ограничению подтипа.
С изменяемыми же всё сложнее, так как следующие две ситуации являются взаимоисключающими для принципа LSP:
Соответственно, остаётся лишь три пути:
TypeScript
В тайпскрипте логика простая: все параметры функции считаются ковариантными (что не верно), а возвращаемые значения — контравариантными (что верно). Ранее было показано, что параметры функции могут иметь совершенно любую вариантность в зависимости от того, что эта функция с этими параметрами делает. Поэтому получаются такие вот казусы:
Чтобы решить эту проблему приходится помогать компилятору довольно нетривиальным кодом:
FlowJS
FlowJS имеет более продвинутую систему типов. В частности, в описании типа можно указать его вариантность для обобщённых параметров и для полей объектов. На нашем примере с клетками выглядит это примерно так:
Бивариантность тут невыразима. К сожалению, мне не удалось найти способа более удобно задавать вариантность не описывая типы всех полей явно. Например, как-то так:
C Sharp
C# изначально проектировался без какого-либо понимания вариантности. Однако, впоследствии в нём были добавлены in и out модификаторы параметров, что позволило компилятору правильно проверять типы передаваемых аргументов. К сожалению, использовать эти модификаторы опять же не очень удобно.
К Java возможность переключения вариантности была добавлена довольно поздно и лишь для обобщённых параметров, которые сами-то появились сравнительно недавно. Если же параметр не обобщённый, то беда.
C++ благодаря мощной системе шаблонов может выразить различные вариантности, но кода, конечно получается много.
D не имеет вменяемых средств явного указания вариантности, зато он умеет сам выводить типы исходя из их использования.
Эпилог
На этом пока всё. Надеюсь изложенный материал помог вам лучше понять ограничения на типы, и как они реализованы в разных языках. Где-то лучше, где-то хуже, где-то никак, но в целом — так себе. Возможно именно вы разработаете язык, в котором всё это будет реализовано и удобно, и типобезопасно. А пока присоединяйтесь к нашему телеграм чату, где мы иногда обсуждаем теоретические концепции языков программирования.