Классы типов: 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 экономит уйму ручной работы.

Проверьте себя
1. Что означает ограничение Ord a => в сигнатуре?
AТип a обязан быть числом
BТип a должен поддерживать сравнение/порядок (быть в классе Ord)
CФункция возвращает упорядоченный список
DАргументов ровно один
2. Зачем нужен deriving (Eq, Show) у своего типа?
AЧтобы запретить сравнение
BЧтобы компилятор сам сгенерировал операции сравнения и печати
CЧтобы ускорить рекурсию
DЧтобы тип стал числом