Иди налево, потом направо

Ещё один чудесный тип, принимающий два других в качестве параметров, – это тип Either. Он определён приблизительно так:

data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

У него два конструктора данных. Если используется конструктор Left, его содержимое имеет тип a; если Right – содержимое имеет тип b. Таким образом, мы можем использовать данный тип для инкапсуляции значения одного из двух типов. Когда мы работаем с типом Either a b, то обычно используем сопоставление с образцом по Left и Right и выполняем действия в зависимости от того, какой вариант совпал.

ghci> Right 20

Right 20

ghci> Left "в00т"

Left "в00т"

ghci> :t Right 'a'

Right 'a' :: Either a Char ghci> :t Left True

Left True :: Either Bool b

Из приведённого примера следует, что типом значения Left True является Either Bool b. Первый параметр типа Bool, поскольку значение создано конструктором Left; второй же параметр остался полиморфным. Ситуация подобна тому как значение Nothing имеет тип Maybe a.

Мы видели, что тип Maybe главным образом используется для того, чтобы представить результат вычисления, которое может завершиться неудачей. Но иногда тип Maybe не так удобен, поскольку значение Nothing не несёт никакой информации, кроме того что что-то пошло не так. Это нормально для функций, которые могут выдавать ошибку только в одном случае – или если нам просто не интересно, как и почему функция «упала». Поиск в отображении типа Data.Map может завершиться неудачей, только если искомый ключ не найден, так что мы знаем, что случилось. Но если нам нужно знать, почему не сработала некоторая функция, обычно мы возвращаем результат типа Either a b, где a – это некоторый тип, который может нам что-нибудь рассказать о причине ошибки, и b – результат удачного вычисления. Следовательно, ошибки используют конструктор данных Left, правильные результаты используют конструктор Right.

Например, в школе есть шкафчики для того, чтобы ученикам было куда клеить постеры Guns’n’Roses. Каждый шкафчик открывается кодовой комбинацией. Если школьнику понадобился шкафчик, он говорит администратору, шкафчик под каким номером ему нравится, и администратор выдаёт ему код. Если этот шкафчик уже кем-либо используется, администратор не сообщает код – они вместе с учеником должны будут выбрать другой вариант. Будем использовать модуль Data.Map для того, чтобы хранить информацию о шкафчиках. Это будет отображение из номера шкафчика в пару, где первый компонент указывает, используется шкафчик или нет, а второй компонент – код шкафчика.

import qualified Data.Map as Map

data LockerState = Taken | Free deriving (Show, Eq)

type Code = String

type LockerMap = Map.Map Int (LockerState, Code)

Довольно просто. Мы объявляем новый тип данных для хранения информации о том, был шкафчик занят или нет. Также мы создаём синоним для кода шкафчика и для типа, который отображает целые числа в пары из статуса шкафчика и кода. Теперь создадим функцию для поиска кода по номеру. Мы будем использовать тип Either String Code для представления результата, так как поиск может не удаться по двум причинам – шкафчик уже занят, в этом случае нельзя сообщать код, или номер шкафчика не найден вообще. Если поиск не удался, возвращаем значение типа String с пояснениями.

lockerLookup :: Int –> LockerMap –> Either String Code

lockerLookup lockerNumber map =

  case Map.lookup lockerNumber map of

   Nothing –> Left $ "Шкафчик № " ++ show lockerNumber ++

                     " не существует!"

   Just (state, code) –>

      if state /= Taken

        then Right code

        else Left $ "Шкафчик № " ++ show lockerNumber ++ " уже занят!"

Мы делаем обычный поиск по отображению. Если мы получили значение Nothing, то вернём значение типа Left String, говорящее, что такой номер не существует. Если мы нашли номер, делаем дополнительную проверку, занят ли шкафчик. Если он занят, возвращаем значение Left, говорящее, что шкафчик занят. Если он не занят, возвращаем значение типа Right Code, в котором даём студенту код шкафчика. На самом деле это Right String, но мы создали синоним типа, чтобы сделать наши объявления более понятными. Вот пример отображения:

lockers :: LockerMap lockers = Map.fromList

  [(100,(Taken,"ZD39I"))

  ,(101,(Free,"JAH3I"))

  ,(103,(Free,"IQSA9"))

  ,(105,(Free,"QOTSA"))

  ,(109,(Taken,"893JJ"))

  ,(110,(Taken,"99292"))

  ]

Давайте попытаемся узнать несколько кодов.

ghci> lockerLookup 101 lockers

Right "JAH3I"

ghci> lockerLookup 100 lockers

Left "Шкафчик № 100 уже занят!"

ghci> lockerLookup 102 lockers

Left "Шкафчик № 102 не существует!"

ghci> lockerLookup 110 lockers

Left "Шкафчик № 110 уже занят!"

ghci> lockerLookup 105 lockers

Right "QOTSA"

Мы могли бы использовать тип Maybe для представления результата, но тогда лишились бы возможности узнать, почему нельзя получить код. А в нашей функции причина ошибки выводится из результирующего типа.