АТД: поля с метками

Многие типы в реальных проектах довольно велики. Взгляните:

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.

Getter и Setter?

Что же представляют собой метки? Фактически, это особые функции, сгенерированные автоматически. Эти функции имеют три предназначения: создавать, извлекать и изменять. Да, я не оговорился, изменять. Но об этом чуть позже, пусть будет маленькая интрига.

Вот как мы создаём значение типа 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 ofPatient’ 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

С другими видами синтаксического сахара мы встретимся ещё не раз, на куда более продвинутых примерах.