Это должно быть хорошо знакомо любому, кто когда-либо писал код на объектно-ориентированном языке, таком как C ++, Java, C # и т. Д. Полиморфизм подтипов относится к классической иерархии типов, включающей наследование и отношения между объектами. Синтаксис, используемый в F # для обозначения таких отношений, требует некоторого привыкания:
В качестве альтернативы вы можете объявить интерфейс
Частным случаем полиморфизма подтипа является, по сути, размеченное объединение. Декомпилируем простой DU:
в эквивалентный код C # показывает, что эта структура данных реализована как базовый класс с тремя производными классами, каждый из которых представляет отдельный случай объединения.
Специальный полиморфизм
Это просто причудливое название концепции, которая также должна быть вам знакома. Это относится к перегруженным функциям - функциям, которые имеют одинаковое имя, но разные типы и / или количество аргументов. В F # обычные функции не могут быть перегружены:
но члены класса (как обычные, так и статические методы) могут:
В этом нет ничего особенного 😃
Параметрический полиморфизм
Вы, наверное, знаете эту концепцию под названием дженерики. Обобщения позволяют указывать типы и функции, которые дополнительно параметризуются некоторыми другими типами. Модуль коллекций из стандартной библиотеки полон хороших примеров параметрического полиморфизма - например, list
- это тип, который может содержать ints
, floats
, Reindeers
, что у вас есть. Вот пример простой общей структуры данных - дерева, определенного как размеченное объединение:
Общий тип 'a
определяется во время компиляции на основе использования
Мы также можем определять функции, которые сами по себе являются универсальными (что означает, что они работают с универсальными типами). Например, мы можем создать функцию, которая проходит по нашему дереву и печатает все элементы:
Полиморфизм строк
Этот более интересен, потому что он не так широко доступен в популярных объектно-ориентированных языках.
Рассмотрим эту ситуацию - вы хотите объявить функцию, которая работает с типами, у которых есть что-то общее, например, предоставляет определенное свойство. Как бы вы подошли к этой задаче?
Один из способов - объявить интерфейс с указанным свойством и завершить его.
Затем вы можете написать функцию, которая работает с типами, реализующими этот интерфейс. Например, вы можете проверить, поместится ли подарок в рождественский чулок:
У этого решения есть два недостатка. Во-первых, невозможно привязать интерфейс к типу, которым вы не владеете (например, к типу из какой-то внешней библиотеки). Вы можете присоединять методы и свойства только с помощью расширений типов. Эту проблему можно обойти, написав тонкий слой адаптера, но это может стать громоздким.
Вторая проблема интерфейсного подхода связана с тем, что информация о типе, переданная в функцию, стирается. Когда объект, над которым работает функция, возвращается из тела функции, у нас больше нет информации о типе объекта - все, что мы знаем, это то, что он реализует определенный интерфейс. Это видно на примере выше. List.choose
возвращает коллекцию IHasVolume
, хотя мы знаем, что они на самом деле относятся к типу XmasGift
.
Полиморфизм строк - это механизм, устраняющий оба этих недостатка одновременно. В F # вы можете использовать функцию, называемую статически разрешенными параметрами типа (SRTP), которая позволяет вам определять функции, которые работают с типами, имеющими определенные свойства. Компилятор анализирует структуру типов и решает, соответствуют ли они определенным ограничениям. В противном случае вы получите ошибку компиляции. Вот как вы можете переписать fitsInTheSock
функцию, чтобы принимать все типы, имеющие свойство с именем Volume
, возвращающее float
:
И так, что здесь происходит?
- Мы сообщаем компилятору, что функция принимает параметр типа
^a
, который должен предоставлять определенное свойство (Volume
в нашем случае). Это так называемое ограничение. - Мы используем это свойство в теле функции
- Затем мы возвращаем объект типа
^a
, заключенный вOption
.
Это позволяет нам выполнить некоторую дополнительную обработку значения, возвращаемого из fitsInTheSock
, без какого-либо преобразования:
Итак, разве это не здорово? Это так, но у этой техники есть и недостатки. Во-первых, SRTP (пока) не работает с методами расширения, что ограничивает их полезность. Синтаксические ошибки и связанные с ними ошибки компилятора также довольно трудно переварить. Я еще не использовал их в своем производственном коде F #, но было бы хорошо знать, что делает эта функция.
Кстати, помните, когда я сказал, что в F # нельзя перегружать функции? Ну, соврал (немного). Вы можете просто inline
их, и они могут быть автоматически преобразованы компилятором в функции SRTP. Это очень хорошо имитирует перегрузку функций:
Эта функция также очень похожа на утиную печать. Разница между SRTP и утиной типизацией заключается в том, что первая проверяет структуру типов во время компиляции, а вторая заботится только о форме переданного объекта во время выполнения. Что подводит нас к…
Статический и динамический полиморфизм
Они несколько отличаются по сравнению с типами полиморфизма, описанными выше. Все ранее упомянутые полиморфизмы были определенными языковыми особенностями. Статические и динамические полиморфизмы описывают волшебство - соответственно, во время компиляции или во время выполнения.
В случае полиморфизма подтипа компилятор не может определить во время компиляции, каким будет тип объекта, виртуальный метод которого вызывается (в конце концов, в этом весь смысл). Итак, это пример динамического полиморфизма. Утиную печать можно также рассматривать как форму динамического полиморфизма.
Все остальные примеры, которые я описал выше, попадают в категорию статического полиморфизма. Перегрузки функций и универсальные шаблоны проверяются во время компиляции, и SRTP даже имеет static в своем имени (Статически разрешенные параметры типа).
Итак, почему все они названы одинаково?
Узнав обо всех этих различных типах полиморфизма, можно задаться вопросом, почему все они сгруппированы под одним и тем же именем? Я считаю, что общим элементом всех этих методов является то, что все они позволяют выполнять некоторые операции со значениями разных типов. Иногда типы должны быть тесно связаны (например, в полиморфизме подтипов), а иногда они могут быть совершенно не связаны друг с другом (например, в утином наборе или SRTP), но основная идея состоит в том, чтобы иметь единый интерфейс, который позволял бы коду вызываемого действуют по разным типам.
Это все, что я могу сказать о полиморфизме. Если вы заметили какие-либо ошибки или знаете какие-то другие типы полиморфизма, о которых я не упоминал, дайте мне знать :)
С Рождеством и счастливым FSharping!