Линзы и призмы в Haskell
Содержание
Введение⌗
Линзы – это функциональные композиционабельные проекции типов, сильно облегчающие доступ к многоуровневым, вложенным структурам данных.
Под “проекцией типа” я здесь имею в виду некоторый тип T
, конструируемый N-арным конструктором значений makeT
, вместе с некоторой
функцией proj_N
, деконструирующей значение T
в его N-ый элемент:
data T = makeT A B C D
proj_A :: T -> A
proj_B :: T -> B
proj_C :: T -> C
proj_D :: T -> D
т.е. обыкновенным аксессором (accessor). Аксессоры компилятор умеет генерировать автоматически с помощью синтаксиса записей (records):
data Address = Address
{ street :: String
, house :: String
, postCode :: Integer
, phone :: Integer
}
> let a = Address "Evergreen" "3-A" 778899 12345678
> house a
"3-A"
> phone a
12345678
> street a ++ " st., " ++ house a
"Evergreen st., 3-A"
> :t street
street :: Address -> String
> -- переезжаем на другую улицу
> let a' = a {street = "Everblue", postCode = 334456}
a' :: Address
Линзы и другие типы из пакета lens (Control.Lens
) обобщают концепцию аксессоров: конкретную линзу можно представить себе как пару геттера (getter, напр. функция house
выше) и сеттера (setter, напр. конструкция a {stree = "Everblue"}
выше, которая преобразует Address
в обновлённый Address
),
совместимых между собой в некотором смысле. Самые фундаментальные операции над линзами таковы:
view :: Lens' a b -> a -> b
set :: Lens' a b -> b -> a -> a
Простые примеры⌗
Грубо говоря, линзы дают нам возможность удобно комбинировать “проекции” с помощью обычной операции композиции .
и выстраивать цепочки наподобие john.address.house и john.address.phone=1112220 из мира объектно-ориентированного программирования:
> a ^. phone
12345678
> a & street .~ "Everpink"
Address {_street = "Everpink", _house = "3-A", _postCode = 778899, _phone = 12345678}
> a & street .~ "Everblue" & phone .~ 334456
Address {_street = "Everblue", _house = "3-A", _postCode = 778899, _phone = 334456}
> a & street .~ "Everblue" & phone .~ a ^. phone.to succ
Address {_street = "Everblue", _house = "3-A", _postCode = 778899, _phone = 12345679}
Где phone
и street
– линзы, а с помощью комбинатора to
из phone
и обычной функции над числами succ
построен более частный тип линз
(Getter
). Инфиксные функции (^.)
и (.~)
являются синонимами view
и set
соответственно; слева от оператора (&)
(обратное
применение функции, cos x == x & cos
) располагается собственно структура данных. Пример с композицией посложнее:
> :m +Data.Map
> let mapFromList = Data.Map.fromList
> let numdict = mapFromList [ (1, [("ru", "odin"), ("en", "one")]), (2, [("ru", "dva"), ("en", "two")]) ]
> numdict ^. at 2 . _Just . to mapFromList . at "ru" . _Just
"dva"
Создание линз⌗
Линзы (соответственно каждой необходимой проекции) можно написать самому, а можно сгенерировать по шаблону с помощью makeLenses
. В этом
случае по соглашению все элементы структуры данных, для которой они генерятся, должны начинаться с символа подчёркивания. Слегка расширенный
пример с адресом:
{-# Language TemplateHaskell #-}
import Control.Lens
data Student = Student
{ _firstName :: String
, _lastName :: String
, _address :: Address
}
data Address = Address
{ _street :: String
, _house :: String
, _postCode :: Integer
, _phone :: Integer
}
data Class = Class
{ _number :: Int
, _students :: [Student]
}
-- использует расширение TemplateHaskell
makeLenses ''Student
makeLenses ''Address
makeLenses ''Class
cl = Class 10
[ Student "John" "Doe" (Address "Evergreen" "1A" 1122 1234567)
, Student "Mary" "Smith" (Address "Evergreen" "2A" 2233 8798763)
, Student "Bob" "Grey" (Address "Everpink" "4C" 7786 112332123)
]
Теперь обычные аксессоры будут называться с подчёркивания (_number
), а за нормальными именами будут закреплены построенные для данного типа
линзы:
> :t number
number :: Functor f => (Int -> f Int) -> Class -> f Class
> :t _number
_number :: Class -> Int
Использование линз⌗
> -- просто имя и телефон каждого студента в классе
> cl ^.. students.traversed.to (\s -> (s ^. firstName, s ^. address.phone))
[("John",1234567),("Mary",8798763),("Bob",112332123)]
> -- поиск студентов с именем Mary или живущих на Evergreen st.
> cl ^.. students.folded.filtered (\s -> s ^. firstName == "Mary" || s ^. address.street == "Evergreen")
[Student {_firstName = "John", _lastName = "Doe", _address = Address {_street = "Evergreen", _house = "1A", _postCode = 1122, _phone = 1234567}},Student {_firstName = "Mary", _lastName = "Smith", _address = Address {_street = "Everblack", _house = "2A", _postCode = 2233, _phone = 8798763}}]
Конструкции вроде s ^. address.street
очень удобны при работе со вложенными друг в друга типами. Оператор (^..)
(инфиксный вариант
toListOf
) извлекает список элементов “под линзой” типа Fold
(то есть то, на что эта линза “показывает” внутри структуры данных). В
частности, в первом случае линза students.traversed
указывает на каждого студента из списка (список траверсабелен), т.е. на тип Student
.
Линза students.traversed.to (\s -> (s ^. firstName, s ^. address.phone))
указывает уже на пары имя-телефон: значения из неё извлекаются
оператором (^..)
.
Замечательная штука!
Линзы, призмы, изоморфизмы⌗
В данном тексте я везде использую слово “линза”, но это не совсем правильно. В пакете lens
определена целая красивая иерархия абстрактных объектов: Equality
, Iso
, Lens
, Prism
и другие. Например, призмы можно “поворачивать”,
получая геттеры “в обратном направлении”. Это действительно очень круто.
Например, _Just
из примеров выше – это призма. Мы можем “смотреть” через неё на объект Maybe a
и “видеть” значение a
(или юнит, если
Nothing
):
> Just "me" ^. _Just
"me"
> Nothing ^. _Just
()
> Just "me" ^.. _Just.traversed.to toUpper
"ME"
А можем “повернуть” её с помощью re
и “видеть” Maybe a
, “смотря” на a
:
> "me" ^. re _Just
Just "me"
> Just "me" ^.. _Just.traversed.to toUpper . re _Just
[Just 'M',Just 'E']
В некотором смысле это как будто если получить (полное) значение типа Student
через Address
(а не наоборот, как обычно): некоторая
сущность, достраивающая целый объект по его части.
Цель данной короткой заметки – поделиться самыми общими сведениями о технике линз, поэтому за кадром осталось очень много всего интересного.
За документацией к Control.Lens
можно обратиться на Hackage или Wiki проекта.