Параметризовать ли машины?
Когда имеет смысл применять типовые параметры? Обычно мы используем их, когда наш тип данных должен уметь сохранять внутри себя любой другой тип, как это делает Maybe a. Если ваш тип – это некоторая «обёртка», использование типов-параметров оправданно. Мы могли бы изменить наш тип данных Car с такого:
data Car = Car { company :: String
, model :: String
, year :: Int
} deriving (Show)
на такой:
data Car a b c = Car { company :: a
, model :: b
, year :: c
} deriving (Show)
Но выиграем ли мы в чём-нибудь? Ответ – вероятно, нет, потому что впоследствии мы всё равно определим функции, которые работают с типом Car String String Int. Например, используя первое определение Car, мы могли бы создать функцию, которая отображает свойства автомобиля в виде понятного текста:
tellCar :: Car –> String
tellCar (Car {company = c, model = m, year = y}) =
"Автомобиль " ++ c ++ " " ++ m ++ ", год: " ++ show y
ghci> let stang = Car {company="Форд", model="Мустанг", year=1967}
ghci> tellCar stang
"Автомобиль Форд Мустанг, год: 1967"
Приятная маленькая функция. Декларация типа функции красива и понятна. А что если Car – это Car a b c?
tellCar :: (Show a) => Car String String a –> String
tellCar (Car {company = c, model = m, year = y}) =
"Автомобиль " ++ c ++ " " ++ m ++ ", год: " ++ show y
Мы вынуждены заставить функцию принимать параметр Car типа (Show a) => Car String String a. Как видите, декларация типа функции более сложна; единственное преимущество, которое здесь имеется, – мы можем использовать любой тип, имеющий экземпляр класса Show, как тип для типовой переменной c.
ghci> tellCar (Car "Форд" "Мустанг" 1967)
"Автомобиль Форд Мустанг, год: 1967"
ghci> tellCar (Car "Форд" "Мустанг" "тысяча девятьсот шестьдесят седьмой")
"Автомобиль Форд Мустанг, год: "тысяча девятьсот шестьдесят седьмой""
ghci> :t Car "Форд" "Мустанг" 1967
Car "Форд" "Мустанг" 1967 :: (Num t) => Car [Char] [Char] t
ghci> :t Car "Форд" "Мустанг" "тысяча девятьсот шестьдесят седьмой"
Car "Форд" "Мустанг" "тысяча девятьсот шестьдесят седьмой"
:: Car [Char] [Char] [Char]
На практике мы всё равно в большинстве случаев использовали бы Car String String Int, так что в параметризации типа Car большого смысла нет. Обычно мы параметризируем типы, когда для работы нашего типа неважно, что в нём хранится. Список элементов – это просто список элементов, и неважно, какого они типа: список работает вне зависимости от этого. Если мы хотим суммировать список чисел, то в суммирующей функции можем уточнить, что нам нужен именно список чисел. То же самое верно и для типа Maybe. Он предоставляет возможность не иметь никакого значения или иметь какое-то одно значение. Тип хранимого значения не важен.
Ещё один известный нам пример параметризованного типа – отображения Map k v из модуля Data.Map. Параметр k – это тип ключей в отображении, параметр v – тип значений. Это отличный пример правильного использования параметризации типов. Параметризация отображений позволяет нам использовать любые типы, требуя лишь, чтобы тип ключа имел экземпляр класса Ord. Если бы мы определяли тип для отображений, то могли бы добавить ограничение на класс типа в объявлении:
data (Ord k) => Map k v = ...
Тем не менее в языке Haskell принято соглашение никогда не использовать ограничения класса типов при объявлении типов данных. Почему? Потому что серьёзных преимуществ мы не получим, но в конце концов будем использовать всё больше ограничений, даже если они не нужны. Поместим ли мы ограничение (Ord k) в декларацию типа или не поместим – всё равно придётся указывать его при объявлении функций, предполагающих, что ключ может быть упорядочен. Но если мы не поместим ограничение в объявлении типа, нам не придётся писать его в тех функциях, которым неважно, может ключ быть упорядочен или нет. Пример такой функции – toList :: Map k a –> [(k, a)]. Если бы Map k a имел ограничение типа в объявлении, тип для функции toList был бы таким: toList :: (Ord k) => Map k a –> [(k, a)], даже несмотря на то что функция не сравнивает элементы друг с другом.
Так что не помещайте ограничения типов в декларации типов данных, даже если это имело бы смысл, потому что вам всё равно придётся помещать ограничения в декларации типов функций.