Многие типы в реальных проектах довольно велики. Взгляните:
data Arguments = Arguments Port
Endpoint
RedirectData
FilePath
FilePath
Bool
FilePathЗначение типа Arguments хранит в своих полях некоторые значения, извлечённые из параметров командной строки, с которыми запущена одна из моих программ. И всё бы хорошо, но работать с таким типом абсолютно неудобно. Он содержит семь полей, и паттерн матчинг был бы слишком громоздким, представьте себе:
...
where
Arguments _ _ _ redirectLib _ _ xpi = argumentsБолее того, когда мы смотрим на определение типа, назначение его полей остаётся тайной за семью печатями. Видите предпоследнее поле? Оно имеет тип Bool и, понятное дело, отражает какой-то флаг. Но что это за флаг, читатель не представляет. К счастью, существует способ, спасающих нас от обеих этих проблем.
Мы можем снабдить наши поля метками (англ. label). Вот как это выглядит:
data Arguments = Arguments { runWDServer :: Port
, withWDServer :: Endpoint
, redirect :: RedirectData
, redirectLib :: FilePath
, screenshotsDir :: FilePath
, noScreenshots :: Bool
, harWithXPI :: FilePath
}Теперь назначение меток куда понятнее. Схема определения такова:
data Arguments = Arguments { runWDServer :: Port }
тип такой-то конструктор метка поля тип
поляТеперь поле имеет не только тип, но и название, что и делает наше определение значительно более читабельным. Поля в этом случае разделены запятыми и заключены в фигурные скобки.
Если подряд идут два или более поля одного типа, его можно указать лишь для последней из меток. Так, если у нас есть вот такой тип:
data Patient = Patient { firstName :: String
, lastName :: String
, email :: String
}его определение можно чуток упростить и написать так:
data Patient = Patient { firstName
, lastName
, email :: String
}Раз тип всех трёх полей одинаков, мы указываем его лишь для последней из меток. Ещё пример полной формы:
data Patient = Patient { firstName :: String
, lastName :: String
, email :: String
, age :: Int
, diseaseId :: Int
, isIndoor :: Bool
, hasInsurance :: Bool
}и тут же упрощаем:
data Patient = Patient { firstName
, lastName
, email :: String
, age
, diseaseId :: Int
, isIndoor
, hasInsurance :: Bool
}Поля firstName, lastName и email имеют тип String, поля age и diseaseId — тип Int, и оставшиеся два поля — тип Bool.
Что же представляют собой метки? Фактически, это особые функции, сгенерированные автоматически. Эти функции имеют три предназначения: создавать, извлекать и изменять. Да, я не оговорился, изменять. Но об этом чуть позже, пусть будет маленькая интрига.
Вот как мы создаём значение типа Patient
main :: IO ()
main = print $ diseaseId patient
where
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "[email protected]"
, age = 24
, diseaseId = 431
, isIndoor = True
, hasInsurance = True
}Метки полей используются как своего рода setter (от англ. set, «устанавливать»):
patient = Patient { firstName = "John"
в этом типа поле с
значении Patient этой меткой равно этой строкеКроме того, метку можно использовать и как getter (от англ. get, «получать»):
main = print $ diseaseId patient
метка как аргумент
функцииМы применяем метку к значению типа Patient и получаем значение соответствующего данной метке поля. Поэтому для получения значений полей нам уже не нужен паттерн матчинг.
Но что же за интригу я приготовил под конец? Выше я упомянул, что метки используются не только для задания значений полей и для их извлечения, но и для изменения. Вот что я имел в виду:
main :: IO ()
main = print $ email patientWithChangedEmail
where
patientWithChangedEmail = patient {
email = "[email protected]" -- Изменяем???
}
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "[email protected]"
, age = 24
, diseaseId = 431
, isIndoor = True
, hasInsurance = True
}При запуске программы получим:
j.d@gmail.comНо постойте, что же тут произошло? Ведь в Haskell, как мы знаем, нет оператора присваивания, однако значение поля с меткой email поменялось. Помню, когда я впервые увидел подобный пример, то очень удивился, мол, уж не ввели ли меня в заблуждение по поводу неизменности значений в Haskell?!
Нет, не ввели. Подобная запись:
patientWithChangedEmail = patient {
email = "[email protected]"
}действительно похожа на изменение поля через присваивание ему нового значения, но в действительности никакого изменения не произошло. Когда я назвал метку setter-ом, я немного слукавил, ведь классический setter из мира ООП был бы невозможен в Haskell. Посмотрим ещё раз внимательнее:
...
where
patientWithChangedEmail = patient {
email = "[email protected]" -- Изменяем???
}
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "[email protected]"
, age = 24
, diseaseId = 431
, isIndoor = True
, hasInsurance = True
}Взгляните, ведь у нас теперь два значения типа Patient, patient и patientWithChangedEmail. Эти значения не имеют друг ко другу ни малейшего отношения. Вспомните, как я говорил, что в Haskell нельзя изменить имеющееся значение, а можно лишь создать на основе имеющегося новое значение. Это именно то, что здесь произошло: мы взяли имеющееся значение patient и на его основе создали уже новое значение patientWithChangedEmail, значение поля email в котором теперь другое. Понятно, что поле email в значении patient осталось неизменным.
Будьте внимательны при инициализации значения с полями: вы обязаны предоставить значения для всех полей. Если вы напишете так:
main :: IO ()
main = print $ email patientWithChangedEmail
where
patientWithChangedEmail = patient {
email = "[email protected]" -- Изменяем???
}
patient = Patient {
firstName = "John"
, lastName = "Doe"
, email = "[email protected]"
, age = 24
, diseaseId = 431
, isIndoor = True
}
-- Поле hasInsurance забыли!код скомпилируется, но внимательный компилятор предупредит вас о проблеме:
Fields of ‘Patient’ not initialised: hasInsuranceПожалуйста, не пренебрегайте подобным предупреждением, ведь если вы проигнорируете его и затем попытаетесь обратиться к неинициализированному полю:
main = print $ hasInsurance patient
...ваша программа аварийно завершится на этапе выполнения с ожидаемой ошибкой:
Missing field in record construction hasInsuranceНе забывайте: компилятор — ваш добрый друг.
Помните, что метки полей — это синтаксический сахар, без которого мы вполне можем обойтись. Даже если тип был определён с метками, как наш Patient, мы можем работать с ним по-старинке:
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"
"[email protected]"
24
431
True
TrueСоответственно, извлекать значения полей тоже можно по-старинке, через паттерн матчинг:
main :: IO ()
main = print insurance
where
-- Жутко неудобно, но если желаете...
Patient _ _ _ _ _ _ insurance = patient
patient = Patient "John"
"Doe"
"[email protected]"
24
431
True
TrueС другими видами синтаксического сахара мы встретимся ещё не раз, на куда более продвинутых примерах.