show Red = "Красный свет"
show Yellow = "Жёлтый свет"
show Green = "Зелёный свет"
Мы снова использовали сопоставление с образцом, чтобы достичь нашей цели. Давайте посмотрим, как это всё работает:
ghci> Red == Red
True
ghci> Red == Yellow
False
ghci> Red `elem` [Red, Yellow, Green]
True
ghci> [Red, Yellow, Green]
[Красный свет,Жёлтый свет,Зелёный свет]
Можно было бы просто автоматически сгенерировать экземпляр для класса Eq
с абсолютно тем же результатом (мы этого не сделали в образовательных целях). Кроме того, автоматическая генерация для класса Show
просто напрямую переводила бы конструкторы значений в строки. Если нам требуется печатать что-то дополнительно, то придётся создавать экземпляр класса Show
вручную.
Наследование классов
Также можно создавать классы типов, которые являются подклассами других классов типов. Декларация класса Num
довольно длинна, но вот её начало:
class (Eq a) => Num a where
...
Как уже говорилось ранее, есть множество мест, куда мы можем втиснуть ограничения на класс. Наша запись равнозначна записи class Num a where
, но мы требуем, чтобы тип a
имел экземпляр класса Eq
. Это означает, что мы должны определить для нашего типа экземпляр класса Eq
до того, как сможем сделать для него экземпляр класса Num
. Прежде чем некоторый тип сможет рассматриваться как число, мы должны иметь возможность проверять значения этого типа на равенство.
Ну вот и всё, что надо знать про наследование, – это просто ограничения на класс типа-параметра при объявлении класса. При написании тел функций в декларации класса или при их определении в экземпляре класса мы можем полагать, что тип a
имеет экземпляр для класса Eq
и, следовательно, допускается использование операторов ==
и /=
со значениями этого типа.
Создание экземпляров классов для параметризованных типов
Но как тип Maybe
и списковый тип сделаны экземплярами классов? Тип Maybe
отличается, скажем, от типа TrafficLight
тем, что Maybe
сам по себе не является конкретным типом – это конструктор типов, который принимает один тип-параметр (например, Char
), чтобы создать конкретный тип (как Maybe Char
). Давайте посмотрим на класс Eq
ещё раз:
class Eq a where
(==) :: a –> a –> Bool
(/=) :: a –> a –> Bool
x == y = not (x /= y)
x /= y = not (x == y)
Из декларации типа мы видим, что a
используется как конкретный тип, потому что все типы в функциях должны быть конкретными (помните, мы обсуждали, что не можем иметь функцию типа a –> Maybe
, но можем – функцию типа: a –> Maybe a
или Maybe Int –> Maybe String
). Вот почему недопустимо делать что-нибудь в таком роде:
instance Eq Maybe where
...
Ведь, как мы видели, идентификатор a
должен принимать значение в виде конкретного типа, а тип Maybe
не является таковым. Это конструктор типа, который принимает один параметр и производит конкретный тип.
Было бы скучно прописывать instance Eq (Maybe Int) where
, instance Eq (Maybe Char) where
и т. д. для всех существующих типов. Вот почему мы можем записать это так:
instance Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
Это всё равно что сказать, что мы хотим сделать для всех типов формата Maybe <
экземпляр класса Eq
. Мы даже могли бы записать (Maybe something)
, но обычно программисты используют одиночные буквы, чтобы придерживаться стиля языка Haskell. Выражение (Maybe m)
выступает в качестве типа a
в декларации class Eq a where
. Тип Maybe
не является конкретным типом, а Maybe m
– является. Указание типа-параметра (m
в нижнем регистре) свидетельствует о том, что мы хотим, чтобы все типы вида Maybe
m
, где m
– любой тип, имели экземпляры класса Eq
.
Однако здесь есть одна проблема. Заметили? Мы используем оператор ==
для содержимого типа Maybe
, но у нас нет уверенности, что то, что содержит тип Maybe
, может быть использовано с методами класса Eq
. Вот почему необходимо поменять декларацию экземпляра на следующую:
instance (Eq m) => Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False