Линзы: Real World

Приветствую, друзья!

Как и было обещано в предыдущей заметке, мы продолжаем беседу о линзах. Впрочем, я чуть-чуть нарушил своё обещание: вместо пакета lens мы рассмотрим пакет fclabels. Дело в том, что я поверил на слово вот этой рекомендации. Пакет fclabels значительно легковеснее, нежели lens, к тому же, что приятно обрадовало лично меня, у него куда как более понятная документация. Разумеется, между этими двумя пакетами есть общие черты. За сим и начнём.

Люди и места

Допустим, есть у нас место:

и есть у нас человек, живущий в этом месте:

Всё предельно просто, перед нами два многопольных типа. Обратите внимание, что имена полей начинаются с символа _. Это не просто так. Ведь мы собираемся использовать пакет fclabels (который вы уже установили, не так ли?), а он требует, чтобы имена полей (к которым мы будем получать доступ через линзы) начинались с подчёркивания. Чуть позже станет понятно, почему.

Теперь весь наш модуль:

Ожидаемый вывод:

Теперь давайте разбираться. Самая необычная строка, которую мы здесь видим - вот эта:

Эта строка есть представитель так называемого Template Haskell. Для тех кто не знает - Template Haskell (сокращённо TH) это специальное расширение языка Haskell, предназначенное для метапрограммирования. Суть его очень проста: специальная конструкция, не являющаяся Haskell-кодом, превращается в некий Haskell-код на этапе компиляции. Часто это избавляет от рутины.

Таким образом, конструкция со словом mkLabels вежливо сгенерирует код наших линз за нас. И вот для того, чтобы это прошло гладко, мы и указали символ _ в именах полей наших типов (таково требование TH). Однако при использовании созданных линз мы уже не видим никаких подчёркиваний, и это очень удобно:

Вот наш старый знакомый, линзовый геттер. Красиво и понятно.

Как вы уже догадались, mkLabels создаёт линзы для нескольких типов одновременно. Если же мы работаем с единственным типом, то можно написать проще:

Комбинирование

Обратите внимание вот на эту строчку:

Геттер используется для доступа к имени города, которое, в свою очередь, спрятано в поле place. Комбинирование полей при линзовом доступе - мощный и удобный механизм. Вспомним комбинационное прочтение: вместо точки добавляем фразу “будет вызвана после”:

Таким образом, place даёт нам доступ к полю типа Place, а city в свою очередь даёт доступ к имени города.

Давайте чуток усложним:

Теперь место жительства содержит поле типа Address. Далее пишем:

Вывод:

Обратите внимание: мы сгенерировали линзы и для типа Address. А теперь комбинируем поля для доступа к названию улицы:

Красиво, не правда ли? Таким образом, линзовый доступ позволяет нам заглянуть сколь угодно глубоко.

Изменяем

Геттеры геттерами, но пришла пора что-нибудь поменять. Изменим возраст:

Итак, Jan помолодел на 6 лет, сеттер set работает в точности как мы и ожидаем.

Разумеется, мы и здесь можем использовать комбинирование полей, дабы заглянуть вглубь. Представим, что наш Jan решил переехать в другой дом на своей улице:

Теперь Jan живёт в доме номер 9. Элегантно и просто.

Модифицируем

Пакет fclabels предоставляет нам ещё один способ линзового изменения, а именно через функцию modify. Если уже известная нам функция set изменяет поле прямым значением, то modify делает это через функцию.

Помните, как мы изменили возраст? Сделаем же это иначе: теперь наш Jan повзрослеет на один год. Пишем:

И как вы уже догадались, мы и тут можем заглянуть вглубь. Пусть наш Jan опять переедет, но уже в другой конец улицы:

Теперь Jan живёт в 18 доме.

Персона в облаке

В модуле Data.Label.Monadic определены вкусности для работы с трансформерами Reader и State. Далее я предполагаю, что вы знакомы с этими трансформерами. Используется пакет mtl.

Поместим нашего Jan в облако, дабы с ним было удобно работать из нескольких функций. Вот наше облако:

Задача такова: вывести на экран информацию о персоне, причём есть несколько функций, каждая из которых вытаскивает лишь часть информации об этой персоне. Вот как это будет выглядеть:

Мы можем работать с облачным значением двумя способами. Рассмотрим на примере функции printAge. Вот первый, канонический способ:

Тут всё как обычно: сначала, используя Reader-функцию ask, мы вытаскиваем нашу персону, а затем, через уже известную нам линзу, получаем возраст. Но есть путь короче:

Круто, правда? Мы использовали функцию asks из модуля Data.Label.Monadic. Она упрощает нам жизнь, сразу же вытаскивая из облачной персоны её возраст.

Таким же образом можно работать и с трансформером State: в модуле Data.Label.Monadic вы найдёте функции gets, puts и modify.

Выводы

Ну что ж, на мой взгляд, пакет fclabels более чем заслуживает нашего внимания. Просто, удобно и элегантно. Кстати, у этого пакета значиииительно меньше зависимостей, нежели у lens.

В будущих заметках мы продолжим рассмотрение линз (из этого пакета или из других), в контексте различных интересных задач.