# Глава 21. АТД: поля с метками
Обсудим, для чего нужно снабжать поля метками (англ. label).
## Проблема
Многие типы в реальных проектах довольно велики. Взгляните:
```haskell
data Arguments = Arguments Port
Endpoint
RedirectData
FilePath
FilePath
Bool
FilePath
```
Значение типа `Arguments` хранит в своих полях некоторые значения, извлечённые из параметров командной строки, с которыми запущена одна из моих программ. И всё бы хорошо, но работать с таким типом абсолютно неудобно. Он содержит семь полей, и паттерн матчинг был бы слишком громоздким, представьте себе:
```haskell
...
where
Arguments _ _ _ redirectLib _ _ xpi = arguments
```
Более того, когда мы смотрим на определение типа, назначение его полей остаётся тайной за семью печатями. Видите предпоследнее поле? Оно имеет тип `Bool` и, понятное дело, отражает какой-то флаг. Но что это за флаг, читатель не представляет. К счастью, существует способ, спасающих нас от обеих этих проблем.
## Метки
Мы можем снабдить наши поля метками. Вот как это выглядит:
```haskell
data Arguments = Arguments { runWDServer :: Port
, withWDServer :: Endpoint
, redirect :: RedirectData
, redirectLib :: FilePath
, screenshotsDir :: FilePath
, noScreenshots :: Bool
, harWithXPI :: FilePath
}
```
Теперь назначение меток куда понятнее. Схема определения такова:
```haskell
data Arguments = Arguments { runWDServer :: Port }
-- тип такой-то конструктор метка поля тип
-- поля
```
Теперь поле имеет не только тип, но и название, что и делает наше определение значительно более читабельным. Поля в этом случае разделены запятыми и заключены в фигурные скобки.
Если подряд идут два или более поля одного типа, его можно указать лишь для последней из меток. Так, если у нас есть вот такой тип:
```haskell
data Patient = Patient { firstName :: String
, lastName :: String
, email :: String
}
```
его определение можно чуток упростить и написать так:
```haskell
data Patient = Patient { firstName
, lastName
, email :: String
}
```
Раз тип всех трёх полей одинаков, мы указываем его лишь для последней из меток. Ещё пример полной формы:
```haskell
data Patient = Patient { firstName :: String
, lastName :: String
, email :: String
, age :: Int
, diseaseId :: Int
, isIndoor :: Bool
, hasInsurance :: Bool
}
```
и тут же упрощаем:
```haskell
data Patient = Patient { firstName
, lastName
, email :: String
, age
, diseaseId :: Int
, isIndoor
, hasInsurance :: Bool
}
```
Поля `firstName`, `lastName` и `email` имеют тип `String`, поля `age` и `diseaseId` — тип `Int`, и оставшиеся два поля — тип `Bool`.
## Getter и Setter?
Что же представляют собой метки? Фактически, это особые функции, сгенерированные автоматически. Эти функции имеют три предназначения:
- создавать,
- извлекать,
- изменять.
Да, я не оговорился, изменять. Но об этом чуть позже, пусть будет маленькая интрига.
Вот как мы создаём значение типа `Patient`:
```haskell {.example_for_playground .example_for_playground_001}
main :: IO ()
main = print $ diseaseId patient
where
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "john.doe@gmail.com"
, age = 24
, diseaseId = 431
, isIndoor = True
, hasInsurance = True
}
```
Метки полей используются как своего рода setter (от англ. set, «устанавливать»):
```haskell
patient = Patient { firstName = "John"
-- в этом типа поле с
-- значении Patient этой меткой равно этой строке
```
Кроме того, метку можно использовать и как getter (от англ. get, «получать»):
```haskell
main = print $ diseaseId patient
-- метка как аргумент
-- функции
```
Мы применяем метку к значению типа `Patient` и получаем значение соответствующего данной метке поля. Поэтому для получения значений полей нам уже не нужен паттерн матчинг.
Но что же за интригу я приготовил под конец? Выше я упомянул, что метки используются не только для задания значений полей и для их извлечения, но и для изменения. Вот что я имел в виду:
```haskell {.example_for_playground .example_for_playground_002}
main :: IO ()
main = print $ email patientWithChangedEmail
where
patientWithChangedEmail = patient {
email = "j.d@gmail.com" -- Изменяем???
}
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "john.doe@gmail.com"
, age = 24
, diseaseId = 431
, isIndoor = True
, hasInsurance = True
}
```
При запуске программы получим:
```haskell
j.d@gmail.com
```
Но постойте, что же тут произошло? Ведь в Haskell, как мы знаем, нет оператора присваивания, однако значение поля с меткой `email` поменялось. Помню, когда я впервые увидел подобный пример, то очень удивился, мол, уж не ввели ли меня в заблуждение по поводу неизменности значений в Haskell?!
Нет, не ввели. Подобная запись:
```haskell
patientWithChangedEmail = patient {
email = "j.d@gmail.com"
}
```
действительно похожа на изменение поля через присваивание ему нового значения, но в действительности никакого изменения не произошло. Когда я назвал метку setter-ом, я немного слукавил, ведь классический setter из мира ООП был бы невозможен в Haskell. Посмотрим ещё раз внимательнее:
```haskell
...
where
patientWithChangedEmail = patient {
email = "j.d@gmail.com" -- Изменяем???
}
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "john.doe@gmail.com"
, age = 24
, diseaseId = 431
, isIndoor = True
, hasInsurance = True
}
```
Взгляните, ведь у нас теперь два значения типа `Patient`, `patient` и `patientWithChangedEmail`. Эти значения не имеют друг ко другу ни малейшего отношения. Вспомните, как я говорил, что в Haskell нельзя изменить имеющееся значение, а можно лишь создать на основе имеющегося новое значение. Это именно то, что здесь произошло: мы взяли имеющееся значение `patient` и на его основе создали уже новое значение `patientWithChangedEmail`, значение поля `email` в котором теперь другое. Понятно, что поле `email` в значении `patient` осталось неизменным.
Будьте внимательны при инициализации значения с полями: вы обязаны предоставить значения для всех полей. Если вы напишете так:
```haskell {.example_for_playground .example_for_playground_003}
main :: IO ()
main = print $ email patientWithChangedEmail
where
patientWithChangedEmail = patient {
email = "j.d@gmail.com" -- Изменяем???
}
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "john.doe@gmail.com"
, age = 24
, diseaseId = 431
, isIndoor = True
}
-- Поле hasInsurance забыли!
```
код скомпилируется, но внимательный компилятор предупредит вас о проблеме:
```
Fields of ‘Patient’ not initialised: hasInsurance
```
Пожалуйста, не пренебрегайте подобным предупреждением, ведь если вы проигнорируете его и затем попытаетесь обратиться к неинициализированному полю:
```haskell
main = print $ hasInsurance patient
...
```
ваша программа аварийно завершится на этапе выполнения с ожидаемой ошибкой:
```
Missing field in record construction hasInsurance
```
Не забывайте: компилятор — ваш добрый друг.
Заведите тип `UserQuery`, описывающий запрос пользователя в поисковике. У него должны быть поля: {.task_text}
- `query`: сам поисковый запрос.
- `clickCount`: количество кликов по результатам поисковой выдачи.
- `usedSuggest`: флаг, означающий, воспользовался ли пользователь подсказчиком в строке запроса.
- `viewedAdd`: флаг, означающий, была ли показана на странице реклама.
{.task_text}
В блоке `where` функции `main` создайте экземпляр `q1` типа `UserQuery`. У него поле `query` равно строке "pancakes", `clickCount` равно 2, `usedSuggest` равно `False`, `viewedAd` равно `True`. {.task_text}
Затем создайте экземпляр `q2`, поля которого равны полям `q1` за исключением `clickCount`: значение этого поля должно быть на 1 больше. {.task_text}
В теле функции `main` выведите в консоль значение поля `clickCount` для `q1` и `q2`. {.task_text}
```haskell {.task_source #haskell_chapter_0210_task_0010}
module Main where
-- UserQuery type definition
main :: IO()
main = do
-- print clickCount of q1
-- print clickCount of q2
print $ query q1
where
q2 = -- q1 with incremented clickCount value
q1 = -- UserQuery with clickCount = 2
```
Синтаксис определения типа: `data TypeName = TypeName { label1 :: Type1, label2 :: Type2, ...}`. {.task_hint}
```haskell {.task_answer}
module Main where
data UserQuery = UserQuery { query :: String
, clickCount :: Int
, usedSuggest
, viewedAd :: Bool
}
main :: IO()
main = do
print $ clickCount q1
print $ clickCount q2
print $ query q1
where
q2 = q1 { clickCount = clickCount q1 + 1 }
q1 = UserQuery {
query = "pancakes"
, clickCount = 2
, usedSuggest = False
, viewedAd = True
}
```
Напишите, является ли тип `UserQuery` из предыдущей задачи Типом Сумма или Типом Произведение: {.task_text}
- `sum`, если это Тип Сумма.
- `product`, если это Тип Произведение.
{.task_text}
```haskell
data UserQuery = UserQuery { query :: String
, clickCount :: Int
, usedSuggest
, viewedAd :: Bool
}
```
```consoleoutput {.task_source #haskell_chapter_0210_task_0020}
```
Мощность типа `UserQuery` равна произведению всех значений, принимаемых его полями. {.task_hint}
```haskell {.task_answer}
product
```
## Без меток
Помните, что метки полей — это синтаксический сахар, без которого мы вполне можем обойтись. Даже если тип был определён с метками, как наш `Patient`, мы можем работать с ним по-старинке:
```haskell
data Patient = Patient { firstName :: String
, lastName :: String
, email :: String
, age :: Int
, diseaseId :: Int
, isIndoor :: Bool
, hasInsurance :: Bool
}
main :: IO ()
main = print $ hasInsurance patient
where
-- Создаём по-старинке...
patient = Patient "John"
"Doe"
"john.doe@gmail.com"
24
431
True
True
```
Соответственно, извлекать значения полей тоже можно по-старинке, через паттерн матчинг.
На строке 15 замените чтение поля через паттерн матчинг на работу с меткой. {.task_text}
```haskell {.task_source #haskell_chapter_0210_task_0030}
module Main where
data Patient = Patient { firstName
, lastName
, email :: String
, age
, diseaseId :: Int
, isIndoor
, hasInsurance :: Bool
}
main :: IO ()
main = print insurance
where
Patient _ _ _ _ _ _ insurance = patient -- Needs refactoring!
patient = Patient "John"
"Doe"
"john.doe@gmail.com"
24
431
False
True
```
Синтаксис обращения по метке: `b = label a`. {.task_hint}
```haskell {.task_answer}
module Main where
data Patient = Patient { firstName
, lastName
, email :: String
, age
, diseaseId :: Int
, isIndoor
, hasInsurance :: Bool
}
main :: IO ()
main = print insurance
where
insurance = hasInsurance patient
patient = Patient "John"
"Doe"
"john.doe@gmail.com"
24
431
False
True
```
С другими видами синтаксического сахара мы встретимся ещё не раз, на куда более продвинутых примерах.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!