Например, в школе есть шкафчики для того, чтобы ученикам было куда клеить постеры 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
для представления результата, но тогда лишились бы возможности узнать, почему нельзя получить код. А в нашей функции причина ошибки выводится из результирующего типа.
Рекурсивные структуры данных
Как мы уже видели, конструкторы алгебраических типов данных могут иметь несколько полей (или не иметь вовсе), и у каждого поля должен быть конкретный тип. Принимая это во внимание, мы можем создать тип, конструктор которого имеет поля того же самого типа! Таким образом мы можем создавать рекурсивные типы данных, где одно значение некоторого типа содержит другие значения этого типа, а они, в свою очередь, содержат ещё значения того же типа, и т. д.
Посмотрите на этот список: [5]
. Это упрощённая запись выражения 5:[]
. С левой стороны от оператора :
ставится значение, с правой стороны – список (в нашем случае пустой). Как насчёт списка [4,5]
? Его можно переписать так: 4:(5:[])
. Смотря на первый оператор :
, мы видим, что слева от него – всё так же значение, а справа – список (5:[])
. То же можно сказать и в отношении списка 3:(4:(5:6:[]))
; это выражение можно переписать и как 3:4:5:6:[]
(поскольку оператор :
правоассоциативен), и как [3,4,5,6]
.