Свои типы данных: data
Ключевое слово data позволяет создать собственный тип, описав, какие значения в нём вообще возможны.
Хотите тип «направление», у которого ровно четыре значения? Или «точку» из двух координат? data описывает это в одну строку — и компилятор гарантирует, что других значений не будет.
Собственные типы делают код выразительным и безопасным. Простейший вид — перечисление: список возможных значений через вертикальную черту:
data Direction = North | South | East | West
turnBack :: Direction -> Direction
turnBack North = South
turnBack South = North
turnBack East = West
turnBack West = East
Имена North, South и так далее — это конструкторы. Значение типа Direction может быть только одним из них. Невозможно создать «пятое направление» — компилятор не позволит.
Конструкторы с полями
Конструктор может нести данные. Так получаются типы-произведения — значение «склеено» из нескольких частей:
data Point = Point Double Double
data Person = Person String Int -- имя и возраст
distanceFromZero :: Point -> Double
distanceFromZero (Point x y) = sqrt (x*x + y*y)
nameOf :: Person -> String
nameOf (Person n _) = n
Здесь Point — и имя типа, и имя конструктора (их часто называют одинаково). Конструктор Point берёт две Double и собирает значение, а сопоставление с образцом (Point x y) достаёт поля обратно.
data Person = Person String Int
| | |
тег имя возраст
Person "Ада" 36 собирает значение
(Person n a) разбирает: n="Ада", a=36
Записи: именованные поля
Когда полей много, удобнее record-синтаксис — он сразу даёт функции-аксессоры:
data User = User
{ userName :: String
, userAge :: Int
}
-- userName и userAge теперь функции:
-- userName (User "Ада" 36) == "Ада"
В Python ближайший аналог — классы данных и перечисления:
# Та же идея на Python: свои типы данных
from dataclasses import dataclass
from enum import Enum
class Direction(Enum):
NORTH = 1; SOUTH = 2; EAST = 3; WEST = 4
@dataclass
class Person:
name: str
age: int
p = Person("Ада", 36)
print(p.name, p.age) # Ада 36
print(Direction.NORTH) # Direction.NORTH
Моделируй домен типами
Главная сила data в том, что точный тип делает неверные состояния попросту непредставимыми. Если статус заказа может быть только «создан», «оплачен» или «отменён», заведите перечисление из трёх конструкторов — и в программе физически не возникнет четвёртого, «невозможного» состояния, которое в языках с числовыми кодами легко породить опечаткой. Типы-суммы (через |) и типы-произведения (конструктор с полями) комбинируются: можно описать «либо ошибка с текстом, либо успех с данными» одним выразительным типом. Record-синтаксис добавляет именованные поля и бесплатные аксессоры, а deriving избавляет от рутины. Подход «сначала смоделируй данные, потом пиши функции» — фирменный стиль Haskell: чем аккуратнее типы отражают предметную область, тем больше ошибок ловит компилятор и тем меньше проверок приходится писать руками.
Как это мыслить
Сначала смоделируйте данные, потом пишите функции. Спросите себя: «какие значения вообще допустимы?» Если ответ — фиксированный набор, делайте перечисление; если комбинация полей — тип-произведение. Чем точнее тип, тем меньше неверных состояний сможет возникнуть.
Стоит добавить, что типы в Haskell могут быть и рекурсивными, и параметризованными — и это открывает дорогу к собственным структурам данных. Например, дерево естественно описывается типом, который ссылается сам на себя: «лист со значением либо узел с двумя поддеревьями». А параметр типа позволяет сделать структуру обобщённой, работающей с элементами любого типа, ровно как встроенный список. Сочетая суммы, произведения, рекурсию и параметры, вы можете смоделировать практически любую предметную область так, что недопустимые состояния окажутся непредставимыми по построению. Это и есть вершина «типобезопасного» проектирования: вместо того чтобы проверять корректность данных в рантайме, вы делаете некорректные данные невыразимыми ещё на уровне типа.
Частые ошибки
- Путать имя типа и конструктора. В
data Point = Point ...первое — тип, второе — способ создать значение; совпадение имён — частая, но допустимая практика. - Забывать
deriving. Чтобы печатать или сравнивать значения, добавьтеderiving (Show, Eq)— об этом в следующем уроке. - Имена конструкторов с маленькой буквы. Типы и конструкторы пишутся с заглавной.
Best practices
- Моделируйте предметную область типами — это убирает «невозможные состояния».
- Для многих полей берите record-синтаксис: читаемее и сразу есть аксессоры.
- Имена типов и конструкторов — с заглавной буквы, это требование языка.
Итог. data создаёт собственные типы: перечисления через |, типы-произведения с полями, записи с именованными полями. Точные типы делают неверные состояния непредставимыми — и в этом огромная сила Haskell.