Классы типов: Eq, Ord, Show
Класс типов — это «интерфейс»: набор операций, который разные типы могут поддерживать. Eq — про сравнение, Ord — про порядок, Show — про печать.
Класс типов отвечает на вопрос «что этот тип умеет?». Если тип в классе Eq — значит, его значения можно сравнивать на равенство. Просто и универсально.
Класс типов (typeclass) — это не класс из ООП. Это набор функций, которые тип обязан предоставить, чтобы «состоять» в этом классе. Самые ходовые классы вы уже встречали неявно.
Три кита: Eq, Ord, Show
- Eq — равенство. Даёт операторы
==и/=. - Ord — порядок. Даёт
<,>,compare,max,min. - Show — превращение в строку через
show(для печати).
Эти классы видны в сигнатурах как ограничения слева от =>:
isEqual :: Eq a => a -> a -> Bool
isEqual x y = x == y
biggest :: Ord a => a -> a -> a
biggest x y = if x > y then x else y
Читается так: «для любого типа a, лежащего в классе Eq, функция берёт два a и возвращает Bool». Ограничение Eq a => — это требование «тип должен уметь сравниваться».
biggest :: Ord a => a -> a -> a
\______/ \________/
ограничение обычная сигнатура
«a должен поддерживать сравнение»
Бесплатные экземпляры через deriving
Для своих типов реализовывать эти операции вручную не нужно — попросите компилятор сгенерировать их через deriving:
data Color = Red | Green | Blue
deriving (Eq, Ord, Show)
-- теперь работает:
-- Red == Red -> True
-- Red < Blue -> True (порядок по объявлению)
-- show Green -> "Green"
Полезен и класс Num — типы-числа (+, *, и т. д.). Поэтому (+) работает и для Int, и для Double: оба в Num.
В Python похожую роль играют «дандер-методы» и протоколы — тип реализует __eq__, __lt__, __repr__, и стандартные операции начинают работать:
# Та же идея на Python: классы типов ~ протоколы/дандеры
from functools import total_ordering
@total_ordering
class Color:
order = {"Red": 0, "Green": 1, "Blue": 2}
def __init__(self, name): self.name = name
def __eq__(self, o): return self.name == o.name # ~ Eq
def __lt__(self, o): return Color.order[self.name] < Color.order[o.name] # ~ Ord
def __repr__(self): return self.name # ~ Show
print(Color("Red") == Color("Red")) # True
print(Color("Red") < Color("Blue")) # True
print(Color("Green")) # Green
Интерфейсы без наследования
Классы типов решают ту же задачу, что интерфейсы в ООП, но иначе и чище: они не привязаны к объявлению типа. Тип может получить экземпляр класса позже и отдельно — даже для чужого типа из библиотеки можно дописать поведение, не трогая его исходники. Это даёт гибкость, которой нет в классическом наследовании. Ограничения вроде Ord a => в сигнатуре работают как декларация требований: функция честно говорит, какие способности нужны от типа, и компилятор не даст применить её к типу без них. Важно держать ограничения минимальными — требуйте только то, что реально используете, иначе функция станет менее универсальной без причины. А deriving для стандартных классов (Eq, Ord, Show) стоит добавлять почти всегда: это бесплатно облегчает отладку, тестирование и сравнение значений.
Как это мыслить
Класс типов — это «способность», а ограничение в сигнатуре — список требуемых способностей. Когда видите Ord a =>, читайте «здесь нужен тип, который умеет сравниваться». Так функция остаётся универсальной, но при этом безопасной: её нельзя применить к типу без нужных операций.
Полезно увидеть, как классы типов делают код одновременно общим и расширяемым. Функция с ограничением Show a => работает с любым типом, умеющим превращаться в строку, — и при этом, добавив новый тип со своим экземпляром Show, вы автоматически получаете возможность использовать его во всех таких функциях, ничего в них не меняя. Это разительно отличается от подхода, где поддержку нового типа пришлось бы дописывать в каждую функцию вручную. Классы типов — это механизм «открытого» полиморфизма: множество типов, удовлетворяющих классу, можно расширять бесконечно и постфактум. Именно на этой идее построена значительная часть стандартной библиотеки и экосистемы, где общие интерфейсы вроде Foldable, Functor и Monad работают сразу с десятками разных типов.
Частые ошибки
- Сравнивать тип без
Eq. Если у типа нет экземпляраEq,==не скомпилируется — добавьтеderiving Eq. - Печатать без
Show.print xтребуетShow; не забудьте его вderiving. - Путать класс типов с ООП-классом. Здесь нет наследования объектов; это интерфейс возможностей.
Best practices
- Почти всегда добавляйте
deriving (Eq, Show)своим типам — пригодится для тестов и отладки. - Указывайте минимально необходимые ограничения: только то, что функция реально использует.
- Если нужен особый порядок, реализуйте
Ordвручную вместо автоматического.
Итог. Классы типов — это интерфейсы способностей: Eq для сравнения, Ord для порядка, Show для печати, Num для чисел. Ограничения вида Eq a => делают функции универсальными и безопасными, а deriving экономит уйму ручной работы.