Это должно быть хорошо знакомо любому, кто когда-либо писал код на объектно-ориентированном языке, таком как 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!