Когда имеет смысл применять типовые параметры? Обычно мы используем их, когда наш тип данных должен уметь сохранять внутри себя любой другой тип, как это делает 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 = ...