Миран Липовача - Изучай Haskell во имя добра!
Модуль Data.Map использует такой подход. Вы не можете создать отображение напрямую при помощи соответствующего конструктора данных, потому что такой конструктор не экспортирован. Однако можно создавать отображения, вызвав одну из вспомогательных функций, например Map.fromList. Разработчики, ответственные за Data.Map, в любой момент могут поменять внутреннее представление отображений, и при этом ни одна существующая программа не сломается.
Разумеется, экспорт конструкторов данных для типов попроще вполне допустим.
Синтаксис записи с именованными полями
Есть ещё один способ определить тип данных. Предположим, что перед нами поставлена задача создать тип данных для описания человека. Данные, которые мы намереваемся хранить, – имя, фамилия, возраст, рост, телефон и любимый сорт мороженого. (Не знаю, как насчёт вас, но это всё, что я хотел бы знать о человеке!) Давайте опишем такой тип:
data Person = Person String String Int Float String String deriving (Show)
Первое поле – это имя, второе – фамилия, третье – возраст и т. д. И вот наш персонаж:
ghci> let guy = Person "Фредди" "Крюгер" 43 184.2 "526–2928" "Эскимо"
ghci> guy
Person "Фредди" "Крюгер" 43 184.2 "526–2928" "Эскимо"
Ну, в целом приемлемо, хоть и не очень «читабельно». Что если нам нужна функция для получения какого-либо поля? Функция, которая возвращает имя, функция для фамилии и т. д.? Мы можем определить их таким образом:
firstName :: Person –> String
firstName (Person firstname _ _ _ _ _) = firstname
lastName :: Person –> String
lastName (Person _ lastname _ _ _ _) = lastname
age :: Person –> Int
age (Person _ _ age _ _ _) = age
height :: Person –> Float
height (Person _ _ _ height _ _) = height
phoneNumber :: Person –> String
phoneNumber (Person _ _ _ _ number _) = number
flavor :: Person –> String
flavor (Person _ _ _ _ _ flavor) = flavor
Фу-ух! Мало радости писать такие функции!.. Этот метод очень громоздкий и скучный, но он работает.
ghci> let guy = Person "Фредди" "Крюгер" 43 184.2 "526–2928" "Эскимо"
ghci> firstName guy
"Фредди"
ghci> height guy
184.2
ghci> flavor guy
"Эскимо"
Вы скажете – должен быть лучший способ! Ан нет, извиняйте, нету… Шучу, конечно же. Такой метод есть! «Ха-ха» два раза. Создатели языка Haskell предусмотрели подобную возможность – предоставили ещё один способ для записи типов данных. Вот как мы можем достигнуть той же функциональности с помощью синтаксиса записей с именованными полями:
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String } deriving (Show)
Вместо того чтобы просто перечислять типы полей через запятую, мы используем фигурные скобки. Вначале пишем имя поля, например firstName, затем ставим два двоеточия :: и, наконец, указываем тип. Результирующий тип данных в точности такой же. Главная выгода – такой синтаксис генерирует функции для извлечения полей. Язык Haskell автоматически создаст функции firstName, lastName, age, height, phoneNumber и flavor.
ghci> :t flavor
flavor :: Person –> String
ghci> :t firstName
firstName :: Person –> String
Есть ещё одно преимущество в использовании синтаксиса записей. Когда мы автоматически генерируем экземпляр класса Show для типа, он отображает тип не так, как если бы мы использовали синтаксис записей с именованными полями для объявления и инстанцирования типа. Например, у нас есть тип, представляющий автомобиль. Мы хотим хранить следующую информацию: компания-производитель, название модели и год производства.
data Car = Car String String Int deriving (Show)
Автомобиль отображается так:
ghci> Car "Форд" "Мустанг" 1967
Car "Форд" "Мустанг" 1967
Используя синтаксис записей с именованными полями, мы можем описать новый автомобиль так:
data Car = Car { company :: String
, model :: String
, year :: Int
} deriving (Show)
Автомобиль теперь создаётся и отображается следующим образом:
ghci> Car {company="Форд", model="Мустанг", year=1967}
Car {company = "Форд", model = "Мустанг", year = 1967}
При создании нового автомобиля мы, разумеется, обязаны перечислить все поля, но указывать их можно в любом порядке. Но если мы не используем синтаксис записей с именованными полями, то должны указывать их по порядку.
Используйте синтаксис записей с именованными полями, если конструктор имеет несколько полей и не очевидно, какое поле для чего используется. Если, скажем, мы создаём трёхмерный вектор: data Vector = Vector Int Int Int, то вполне понятно, что поля конструктора данных – это компоненты вектора. Но в типах Person и Car назначение полей совсем не так очевидно, и мы значительно выиграем, используя синтаксис записей с именованными полями.
Параметры типа
Конструктор данных может принимать несколько параметров-значений и возвращать новое значение. Например, конструктор 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, который может содержать данные вообще любых типов!