Конструктор данных может принимать несколько параметров-значений и возвращать новое значение. Например, конструктор Car
принимает три значения и возвращает одно – экземпляр типа Car
. Таким же образом конструкторы типа могут принимать типы-параметры и создавать новые типы. На первый взгляд это несколько абстрактно, но на самом деле не так уж сложно. Если вы знакомы с шаблонами в языке С++, то увидите некоторые параллели. Чтобы получить более ясное представление о том, как работают типы-параметры, давайте посмотрим, как реализованы типы, с которыми мы уже встречались.
data Maybe a = Nothing | Just a
В данном примере идентификатор a
– тип-параметр (переменная типа, типовая переменная). Так как в выражении присутствует тип-параметр, мы называем идентификатор Maybe
конструктором типов. В зависимости от того, какой тип данных мы хотим сохранять в типе Maybe
, когда он не Nothing
, конструктор типа может производить такие типы, как Maybe Int
, Maybe Car
, Maybe String
и т. д. Ни одно значение не может иметь тип «просто Maybe
», потому что это не тип как таковой – это конструктор типов. Для того чтобы он стал настоящим типом, значения которого можно создать, мы должны указать все типы-параметры в конструкторе типа.
Итак, если мы передадим тип Char
как параметр в тип Maybe
, то получим тип Maybe Char
. Для примера: значение Just 'a'
имеет тип Maybe Char
.
Обычно нам не приходится явно передавать параметры конструкторам типов, поскольку в языке Haskell есть вывод типов. Поэтому когда мы создаём значение Just 'a'
, Haskell тут же определяет его тип – Maybe Char
.
Если мы всё же хотим явно указать тип как параметр, это нужно делать в типовой части выражений, то есть после символа ::
. Явное указание типа может понадобиться, если мы, к примеру, хотим, чтобы значение Just 3
имело тип Maybe Int
. По умолчанию Haskell выведет тип (Num a) => Maybe a
. Воспользуемся явным аннотированием типа:
ghci> Just 3 :: Maybe Int
Just 3
Может, вы и не знали, но мы использовали тип, у которого были типы-параметры ещё до типа Maybe
. Этот тип – список. Несмотря на то что дело несколько скрывается синтаксическим сахаром, конструктор списка принимает параметр для того, чтобы создать конкретный тип. Значения могут иметь тип [Int]
, [Char]
, [[String]]
, но вы не можете создать значение с типом []
.
ПРИМЕЧАНИЕ. Мы называем тип конкретным, если он вообще не принимает никаких параметров (например, Int
или Bool
) либо если параметры в типе заполнены (например, Maybe Char
). Если у вас есть какое-то значение, у него всегда конкретный тип.
Давайте поиграем с типом Maybe
:
ghci> Just "Ха-ха"
Just "Ха-ха"
ghci> Just 84
Just 84
ghci> :t Just "Ха-ха"
Just "Ха-ха" :: Maybe [Char]
ghci> :t Just 84
Just 84 :: (Num t) => Maybe t
ghci> :t Nothing
Nothing :: Maybe a
ghci> Just 10 :: Maybe Double
Just 10.0
Типы-параметры полезны потому, что мы можем с их помощью создавать различные типы, в зависимости от того, какой тип нам надо хранить в нашем типе данных. К примеру, можно объявить отдельные Maybe-подобные типы данных для любых типов:
data IntMaybe = INothing | IJust Int
data StringMaybe = SNothing | SJust String
data ShapeMaybe = ShNothing | ShJust Shape
Более того, мы можем использовать типы-параметры для определения самого обобщённого Maybe
, который может содержать данные вообще любых типов!
Обратите внимание: тип значения Nothing
– Maybe a
. Это полиморфный тип: в его имени присутствует типовая переменная – конкретнее, переменная a
в типе Maybe a
. Если некоторая функция принимает параметр типа Maybe Int
, мы можем передать ей значение Nothing
, так как оно не содержит значения, которое могло бы этому препятствовать. Тип Maybe a
может вести себя как Maybe Int
, точно так же как значение 5
может рассматриваться как значение типа Int
или Double
. Аналогичным образом тип пустого списка – это [a]
. Пустой список может вести себя как список чего угодно. Вот почему можно производить такие операции, как [1,2,3] ++ []
и ["ха","ха","ха"] ++ []
.
Параметризовать ли машины?