Полиморфизм – это идея определения структур данных или алгоритмов в целом, чтобы вы могли использовать их для более чем одного типа данных. Однако полный ответ немного более детализирован. Здесь я собрал различные формы полиморфизма от распространенных типов, которые вы, скорее всего, уже использовали, до менее распространенных, и сравнил, как они выглядят в объектно-ориентированных или функциональных языках.
Параметрический полиморфизм
Это довольно распространенный метод на многих языках, хотя и более известный как “Дженерики”. Основная идея состоит в том, чтобы позволить программистам использовать подстановочный тип при определении структур данных, которые впоследствии могут быть заполнены любым типом. Вот как это выглядит, например, в Java:
class List{ class Node { T data; Node next; } public Node head; public void pushFront(T data) { /* ... */ } }
T
– это переменная типа, потому что позже вы можете “назначить” любой тип, который захотите:
ListmyNumberList = new List (); myNumberList.pushFront("foo"); myNumberList.pushFront(8) // Error: 8 is not a string
Здесь список может содержать только элементы типа string и ничего больше. Мы получаем полезные ошибки компилятора, если пытаемся это нарушить. Кроме того, нам не нужно было снова определять список для каждого возможного типа данных, потому что мы можем просто определить его для всех возможных типов.
Но параметрический полиморфизм присущ не только императивным или объектно-ориентированным языкам, он также очень распространен в функциональном программировании. Например, в Haskell список определяется следующим образом:
data List a = Nil | Cons a (List a)
Это определение означает: Список принимает параметр типа a
(все, что осталось от знака равенства, определяет тип) и является либо пустым списком ( Nil
), либо элементом типа a
и списком типа List a
. Нам не нужен никакой внешний push Front
метод, потому что второй конструктор уже делает это:
let emptyList = Nil oneElementList = Cons "foo" emptyList twoElementList = Cons 8 oneElementList -- Error: 8 is not a string
Специальный полиморфизм
Это более широко известно как перегрузка функций или операторов. В языках, которые допускают это, вы можете определять функцию несколько раз для работы с различными типами ввода. Например, в Java:
class Printer { public String prettyPrint(int x) { /* ... */ } public String prettyPrint(char c) { /* ... */ } }
Компилятор автоматически выберет правильный метод в зависимости от типа данных, которые вы ему передаете. Это может упростить использование API-интерфейсов, поскольку вы можете просто вызывать одну и ту же функцию с любым типом, и вам не нужно запоминать кучу вариантов для разных типов (à la print_string
, print_int
и т.д.).
В Haskell специальный полиморфизм работает через классы типов. Классы типов немного похожи на интерфейсы в объектно-ориентированных языках. Смотрите здесь, например, тот же симпатичный принтер:
class Printer p where prettyPrint :: p -> String instance Printer Int where prettyPrint x = -- ... instance Printer Char where prettyPrint c = -- ...
Полиморфизм подтипов
Подтипирование более известно как объектно-ориентированное наследование. Классическим примером является тип транспортного средства, здесь, на Java:
abstract class Vehicle { abstract double getWeight(); } class Car extends Vehicle { double getWeight() { return 10.0; } } class Truck extends Vehicle { double getWeight() { return 100.0; } } class Toyota extends Car { /* ... */ } static void printWeight(Vehicle v) { // Allowed because all vehicles have to have this method System.out.println(v.getWeight()); }
Здесь мы можем использовать любой дочерний класс класса vehicle как если бы это был непосредственно класс транспортного средства. Обратите внимание, что мы не можем пойти другим путем, потому что, например, не каждое транспортное средство гарантированно является автомобилем.
Это отношение становится немного запутанным, когда вам разрешено передавать функции по кругу, например, здесь, в Typescript:
const driveToyota = (c: Toyota) => { /* ... */ }; const driveVehicle = (c: Vehicle) => { /* ... */ }; function driveThis(f: (c: Car) => void): void { /* ... */ }
Какую из двух функций вам разрешено передавать в управлять Этим
? Вы можете подумать, что первый, в конце концов, как мы видели выше, функции, которая ожидает объект, также могут быть переданы его подклассы (см. Метод printWeight
). Но это неправильно, если вы передаете функцию . Вы можете думать об этом так: управлять этим
хочет что-то, что может принять любой автомобиль. Но если вы передадите drive Toyota
, функция может работать только с Toyotas, чего недостаточно. С другой стороны, если вы передаете функцию, которая может управлять любым транспортным средством ( (
управлять транспортным средством ), сюда также входят автомобили, так
drive This
Поскольку Haskell не является объектно-ориентированным, подтипирование не имело бы особого смысла.
Полиморфизм строк
Теперь мы переходим к менее часто используемым типам полиморфизма, которые реализованы только в очень немногих языках.
Полиморфизм строк подобен младшему брату подтипирования. Вместо того, чтобы указывать, что каждый объект в форме { a:: A, b:: B }
также является a { a:: A }
, мы разрешаем указывать расширение строки: { a:: A | r }
. Это упрощает использование в функциях, так как вам не нужно думать, разрешено ли вам передавать более конкретный или более общий тип, а вместо этого вы просто проверяете, соответствует ли введенный вами тип шаблону. Итак { a:: A, b:: B }
соответствует { a:: A | r }
но { b:: B, c:: C}
этого не делает. Это также имеет то преимущество, что вы не теряете информацию. Если вы применяете Car
к Vehicle
, вы теряете информацию о том, каким конкретным транспортным средством был объект. С расширением строки вы сохраняете всю информацию.
printX :: { x :: Int | r } -> String printX rec = show rec.x printY :: { y :: Int | r } -> String printY rec = show rec.y -- type is inferred as `{x :: Int, y :: Int | r } -> String` printBoth rec = printX rec ++ printY rec
Одним из самых популярных языков, реализующих полиморфизм строк, является PureScript и я также в настоящее время работаю над переносом его на Haskell .
Полиморфизм рода
Виды – это своего рода типы типов. Мы все знаем значения, это данные, с которыми имеет дело каждая функция. 5
, "foo"
, false
– все это примеры значений. Затем есть уровень типа, описывающий значения. Это также должно быть хорошо известно программистам. Типы трех предыдущих значений следующие: Int
, Строка
и Книга
. Но есть даже уровень выше этого: Виды. Вид всех типов – Type
также записывается как *
. Таким образом, это означает 5:: Int:: Тип
( ::
означает “имеет тип”). Есть и другие виды. Например, в то время как наш предыдущий тип списка – это тип ( List a
), что такое List
(без a
)? Ему все еще нужен другой тип в качестве аргумента, чтобы сформировать обычный тип. Следовательно , его вид – List:: Type -> Type
. Если вы даете List
другой тип (например, Int
) вы получаете новый тип ( List Int
).
Полиморфизм видов – это когда вы можете определить тип только один раз, но все равно использовать его с несколькими типами. Лучшим примером является тип данных Proxy
в Haskell. Он используется для “пометки” значения типом:
data Proxy a = ProxyValue let proxy1 = (ProxyValue :: Proxy Int) -- a has kind `Type` let proxy2 = (ProxyValue :: Proxy List) -- a has kind `Type -> Type`
Полиморфизм более высокого ранга
Иногда обычного специального полиморфизма недостаточно. С помощью специального полиморфизма вы предоставляете множество реализаций для разных типов, и потребитель вашего API выбирает, какой тип он хочет использовать. Но иногда вы, как производитель API, хотите выбрать, какую реализацию вы хотите использовать. Вот где вам нужен полиморфизм более высокого ранга. В Haskell это выглядит следующим образом:
-- ad-hoc polymorphism f1 :: forall a. MyTypeClass a => a -> String f1 = -- ... -- higher-rank polymorphism f2 :: Int -> (forall a. MyTypeClass a => a -> String) -> Int f2 = -- ...
Вместо того, чтобы иметь для всех
на самом внешнем месте, мы помещаем его внутрь и поэтому объявляем: Передайте мне функцию, которая может работать с любым типом a
, который реализует MyTypeClass
. Вероятно, вы можете видеть, что f1
является такой функцией, поэтому вам разрешено передавать ее в f2
.
Стандартным примером того, почему это полезно, является так называемый “ST-Trick”. Это то, что позволяет Haskell иметь изменяемое состояние, которое не может выйти за пределы области видимости:
doSomething :: ST s Int doSomething = do ref <- newSTRef 10 x <- readSTRef ref writeSTRef (x + 7) readSTRef ref
Параметр s
здесь важен, для каждой операции с отслеживанием состояния требуется один и тот же s
в подписи. Волшебство теперь заключается в функции, которая преобразует вычисление с сохранением состояния в чистое, запускает
:
runST :: forall a. (forall s. ST s a) -> a
Вы можете видеть, что s
создается с использованием полиморфизма более высокого ранга. Поскольку мы не указываем никаких ограничений на что то s
то есть вычисление с сохранением состояния ничего не может с ним сделать, кроме как передавать его в сигнатурах типов. Он ведет себя точно так же, как “тег” Proxy
. И поскольку он определен только в области действия с сохранением состояния (внутренним forall
), каждая попытка утечки чего-либо из вычисления является ошибкой компилятора.
Линейность полиморфизм
Полиморфизм линейности связан с линейными типами, то есть типами, которые отслеживают “использование” данных. Линейный тип отслеживает так называемую множественность некоторых данных. В общем, вы различаете 3 разных кратности: “ноль”, для материала, который существует только на уровне типа и не разрешается использовать на уровне значений; “один”, для данных, которые не разрешается дублировать (примером могут быть файловые дескрипторы) и “много”, который является для всех других данных.
Линейные типы полезны для обеспечения использования ресурсов. Например, если вы складываете
над изменяемым массивом на функциональном языке. Обычно вам придется копировать массив на каждом шаге или использовать низкоуровневые небезопасные функции. Но с линейными типами вы можете гарантировать, что этот массив может использоваться только в одном месте одновременно, поэтому никаких скачков данных произойти не может.
Аспект полиморфизма вступает в игру с такими функциями, как упомянутая папка . Если вы дадите fold функции, которая использует свой аргумент только один раз, весь
fold
будет использовать начальное значение только один раз. Если вы передаете функцию, которая использует аргумент несколько раз, начальное значение также будет использоваться несколько раз. Полиморфизм линейности позволяет вам определять функцию только один раз и по-прежнему предлагать эту гарантию.
Линейные типы похожи на средство проверки заимствований Rust, но на самом деле Rust не обладает линейным полиморфизмом. Хаскелл скоро получит линейные типы и полиморфизм .
Полиморфизм легкомыслия
В Haskell все обычные типы данных – это просто ссылки на кучу, как и в Python или Java. Но Haskell позволяет также использовать так называемые “unlifted” типы, например, непосредственно машинные целые числа. Это означает, что Haskell фактически кодирует расположение и расположение памяти (стек или куча) типов данных на уровне типов! Они могут быть использованы для дальнейшей оптимизации кода, поэтому процессору не нужно сначала загружать ссылку, а затем запрашивать данные из (медленной) оперативной памяти.
Полиморфизм легкомыслия – это когда вы определяете функции, которые работают как с поднятыми, так и с не поднятыми типами.
Оригинал: “https://dev.to/jvanbruegge/what-the-heck-is-polymorphism-nmh”