Параметрический полиморфизм
Маленькая буква в сигнатуре — это «любой тип». Функция id :: a -> a работает для чисел, строк, списков — для чего угодно.
Параметрический полиморфизм — это дженерики Haskell. Одна функция, написанная для «любого типа», без дублирования кода под каждый конкретный.
Когда в сигнатуре встречается строчная буква (a, b, c), это переменная типа — заглушка для любого конкретного типа. Так выражается параметрический полиморфизм: функция одинаково работает с любым типом.
identity :: a -> a
identity x = x
firstOf :: (a, b) -> a
firstOf (x, _) = x
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
identity принимает значение любого типа и возвращает его же. swap меняет местами элементы пары любых типов. Код один — а применять можно к чему угодно.
Чем меньше тип знает, тем честнее функция
Удивительное следствие: чем абстрактнее тип, тем меньше функция может «начудить». Если сигнатура — a -> a, функция не знает, что за тип a, и потому может только вернуть тот же аргумент. Никаких других вариантов просто нет.
length :: [a] -> Int
списки ЧЕГО УГОДНО -> число
length [1,2,3] == 3
length "abc" == 3
length [True,False] == 2
(длина не зависит от типа элементов)
Сравните с ограниченным полиморфизмом из прошлого урока: там Eq a => добавлял типу способности. Здесь же ограничений нет вовсе — значит, тип абсолютно любой, и функция работает структурно, не заглядывая внутрь значений.
-- работает для списка любого типа:
reverse :: [a] -> [a]
take :: Int -> [a] -> [a]
map :: (a -> b) -> [a] -> [b] -- даже два разных типа!
В Python обобщённость встроена в динамическую природу языка, а аннотации TypeVar делают её явной — прямая аналогия переменных типа:
# Та же идея на Python: дженерики через TypeVar
from typing import TypeVar, Tuple
A = TypeVar("A"); B = TypeVar("B")
def identity(x: A) -> A:
return x
def swap(pair: Tuple[A, B]) -> Tuple[B, A]:
x, y = pair
return (y, x)
print(identity(42)) # 42
print(identity("hi")) # hi
print(swap((1, "a"))) # ('a', 1)
Меньше знаешь — крепче спишь
В параметрическом полиморфизме скрыт красивый парадокс: чем меньше функция знает о типе, тем больше про неё известно нам. Сигнатура a -> a настолько абстрактна, что у функции просто нет выбора — она обязана вернуть свой аргумент, ведь создать значение неизвестного типа из ничего невозможно. Это свойство называют «параметричностью», и оно позволяет угадывать поведение функции по одному её типу, не заглядывая в реализацию. Практическая отдача огромна: полиморфные функции работают для любых типов без дублирования кода, а значит, библиотеки получаются компактными и переиспользуемыми. Когда же нужна конкретная способность — сравнение, печать, арифметика — её добавляют точечно через ограничение класса. Так Haskell сочетает максимальную обобщённость с безопасностью: универсально там, где можно, и строго там, где нужно.
Как это мыслить
Видите строчную букву — читайте «любой тип, мне всё равно какой». Это сигнал, что функция работает со структурой, а не с конкретными значениями. Часто по такой сигнатуре можно угадать поведение: a -> a почти наверняка просто возвращает аргумент.
Стоит уточнить и соотношение двух видов полиморфизма, которые вы уже встретили. Параметрический полиморфизм (строчная буква без ограничений) означает «работает для любого типа одинаково, не заглядывая внутрь значений». Полиморфизм по классам типов (ограничение вроде Eq a =>) означает «работает для любого типа, обладающего нужной способностью». Первый даёт максимальную общность и предсказуемость, второй — гибкость через дополнительные операции. Хорошая функция использует ровно столько, сколько нужно: если задача чисто структурная, оставляйте тип полностью свободным; если требуется сравнение или печать, добавляйте минимальное ограничение. Это сочетание «общее по умолчанию, конкретное по необходимости» — фирменный баланс системы типов Haskell, позволяющий писать одновременно универсальный и безопасный код.
Частые ошибки
- Думать, что
a— конкретный тип. Это переменная: при вызове она подставляется под реальный тип аргумента. - Пытаться «заглянуть внутрь»
a. Без ограничений вы не знаете о типе ничего и не можете, например, сравнить его значения. - Писать
Aс заглавной. Конкретные типы — с заглавной, переменные типа — со строчной.
Best practices
- Делайте функции максимально полиморфными — они становятся переиспользуемыми.
- Добавляйте ограничение (
Eq,Ord...) только когда действительно нужна способность. - Угадывайте поведение по сигнатуре: это отличная тренировка «типового мышления».
Итог. Строчные буквы в сигнатурах — переменные типа, дающие параметрический полиморфизм. Одна функция работает для любых типов, а чем абстрактнее сигнатура, тем предсказуемее поведение. Это дженерики Haskell в самой чистой форме.