Полиморфизм бывает трёх типов: параметрический, ad-hoc и полморфизм подтипов.
Параметрический полиморфизм нужен чтобы писать логику, параметризованную типами, реализующими некоторые интерфейсы (вырожденный случай - параметризованные любыми типами). Простейший пример: функция
map
, применяющая функцию к каждому элементу массива. Без полиморфизма мы бы писали отдельную функцию для каждого типа:
mapIntFloat :: (Int -> Float) -> [Int] -> [Float]
mapIntFloat f [] = []
mapIntFloat f (x:xs) = (f x) : (map f xs)
mapIntString :: (Int -> String) -> [Int] -> [String]
mapIntString f [] = []
mapIntString f (x:xs) = (f x) : (map f xs)
-- и так далее
Выходит довольно громоздко и налицо повторение логики в каждой функции вида
mapTypeType
. Чтобы избежать повторения логики, параметризуем функцию
map
относительно типов исходного массива и массива, получаемого в результате:
map :: forall a b. (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = (f x) : (map f xs)
Теперь вместо конкретных типов вроде
Int
или
Float
у нас
переменные типов:
a
и b.
Окей, но если копнуть глубже, оказывается, что логика, похожая на
map
для массивов, применима и к другим типам. Например, к optional-значениям. Без ad-hoc полиморфизма мы напишем что-то типа:
mapList :: forall a b. (a -> b) -> [a] -> [b]
mapList f [] = []
mapList f (x:xs) = (f x) : (mapList f xs)
mapMaybe :: forall a b. (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing = Nothing
mapMaybe f (Just x) = Just (f x)
Видно, что логика везде примерно одна и та же, сигнатуры совпадают с точностью до параметризванного типа. Вот для этих случаев и нужен ad-hoc полиморфизм:
class Functor f where
map :: (a -> b) -> f a -> f b
Мы параметризовали нужную нам логику относительно параметризованного типа
f
и теперь можем писать реализации для конкретных типов:
instance Functor Maybe where
map f Nothing = Nothing
map f (Just x) = Just (f x)
instance Functor [] where
map f [] = []
map f (x:xs) = (f x) : (map f xs)
Сама функция
map
теперь имеет тип:
map :: forall f a b. Functor f => (a -> b) -> f a -> f b
Теперь мы можем писать функции, которые работают с
любыми функторами, то есть опять-таки сократить повторение логики в коде.
Наконец, полиморфизм подтипов - это несколько другой подход к предыдущей проблеме. Вместо выделения общего интерфейса мы создаём базовый тип и наследуем от него остальные. В этом случае
Functor
будет абстрактным классом с абстрактным методом
map
, от которого наследуются типы
Maybe
,
List
и т.д. В таком случае сигнатура функции, принимающей и возвращающей функтор, будет выглядеть примерно так:
foo :: Functor Int -> Functor String
.