Тем не менее в языке Haskell принято соглашение никогда не использовать ограничения класса типов при объявлении типов данных. Почему? Потому что серьёзных преимуществ мы не получим, но в конце концов будем использовать всё больше ограничений, даже если они не нужны. Поместим ли мы ограничение (Ord k)
в декларацию типа или не поместим – всё равно придётся указывать его при объявлении функций, предполагающих, что ключ может быть упорядочен. Но если мы не поместим ограничение в объявлении типа, нам не придётся писать его в тех функциях, которым неважно, может ключ быть упорядочен или нет. Пример такой функции – toList :: Map k a –> [(k, a)]
. Если бы Map k a
имел ограничение типа в объявлении, тип для функции toList
был бы таким: toList :: (Ord k) => Map k a –> [(k, a)]
, даже несмотря на то что функция не сравнивает элементы друг с другом.
Так что не помещайте ограничения типов в декларации типов данных, даже если это имело бы смысл, потому что вам всё равно придётся помещать ограничения в декларации типов функций.
Векторы судьбы
Давайте реализуем трёхмерный вектор и несколько операций для него. Мы будем использовать параметризованный тип, потому что хоть вектор и содержит только числовые параметры, он должен поддерживать разные типы чисел.
data Vector a = Vector a a a deriving (Show)
vplus :: (Num a) => Vector a –> Vector a –> Vector a
(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)
scalarProd :: (Num a) => Vector a –> Vector a –> a
(Vector i j k) `scalarProd` (Vector l m n) = i*l + j*m + k*n
vmult :: (Num a) => Vector a –> a –> Vector a
(Vector i j k) `vmult` m = Vector (i*m) (j*m) (k*m)
Функция vplus
складывает два вектора путём сложения соответствующих координат. Функция scalarProd
используется для вычисления скалярного произведения двух векторов, функция vmult
– для умножения вектора на константу.
Эти функции могут работать с типами Vector Int
, Vector Integer
, Vector Float
и другими, до тех пор пока тип-параметр a
из определения Vector
a
принадлежит классу типов Num
. По типам функций можно заметить, что они работают только с векторами одного типа, и все координаты вектора также должны иметь одинаковый тип. Обратите внимание на то, что мы не поместили ограничение класса Num
в декларацию типа данных, так как нам всё равно бы пришлось повторять его в функциях.
Ещё раз повторю: очень важно понимать разницу между конструкторами типов и данных. При декларации типа данных часть объявления до знака =
представляет собой конструктор типа, а часть объявления после этого знака – конструктор данных (возможны несколько конструкторов, разделённых символом |
). Попытка дать функции тип Vector a a a -> Vector a a a -> a
будет неудачной, потому что мы должны помещать типы в декларацию типа, и конструктор типа для вектора принимает только один параметр, в то время как конструктор данных принимает три. Давайте поупражняемся с нашими векторами:
ghci> Vector 3 5 8 `vplus` Vector 9 2 8
Vector 12 7 16
ghci> Vector 3 5 8 `vplus` Vector 9 2 8 `vplus` Vector 0 2 3
Vector 12 9 19
ghci> Vector 3 9 7 `vmult` 10
Vector 30 90 70
ghci> Vector 4 9 5 `scalarProd` Vector 9.0 2.0 4.0
74.0
ghci> Vector 2 9 3 `vmult` (Vector 4 9 5 `scalarProd`
Vector 9 2 4) Vector 148 666 222
Производные экземпляры
В разделе «Классы типов» главы 2 приводились базовые сведения о классах типов. Мы упомянули, что класс типов – это нечто вроде интерфейса, который определяет некоторое поведение. Тип может быть сделан экземпляром класса, если поддерживает это поведение. Пример: тип Int
есть экземпляр класса типов Eq
, потому что класс Eq
определяет поведение для сущностей, которые могут быть проверены на равенство. Так как целые числа можно проверить на равенство, тип Int
имеет экземпляр для класса Eq
. Реальная польза от этого видна при использовании функций, которые служат интерфейсом класса Eq
, – операторов ==
и /=
. Если тип имеет определённый экземпляр класса Eq
, мы можем применять оператор ==
к значениям этого типа. Вот почему выражения 4 == 4
и "раз" /= "два"
проходят проверку типов.