Линзы: Hello World!

Приветствую!

В общем, надоело мне, друзья! Сто раз я уже слышал про эти линзы, а что они такое, понятия не имел. Давайте же наконец разберёмся.

В основу данной заметки лёг мой вольный перевод прекрасной статьи, с некоторыми изменениями.

Атлет

Есть у нас атлет, и есть у него имя. Определим следующий тип:

И захотелось нам изменять/получать имя атлета. Определяем две функции:

Теперь главный код:

Перед нами - классические геттер (getName) и сеттер (setName). Всё предельно просто. Однако в реальном проекте так делать не рекомендуется. Рано или поздно у атлета вслед за именем появится дата рождения, рост, знак Зодиака, любимое блюдо, девичья фамилия мамы и т.п. И что же, на каждое такое поле определять свою пару геттер/сеттер?? Нет, нас категорически не устраивает такое решение.

Упрощаем

Автоматизация - наше всё. Пишем:

Вооот, так уже значительно лучше. Компилятор сделал скучную работу за нас. Теперь у нас есть поле name, которое уже является и сеттером, и геттером. Ура, победа! Но, как вы видите, заметка на этом не заканчивается, ибо имеется ещё одна проблема.

Одноимённые поля

Атлеты атлетами, но вскоре у нас появился клуб (возможно, атлетов):

Увы и ах, компилятор грубо оборвёт наше счастье следующей ошибкой:

К сожалению, нельзя использовать одно и то же поле name в подобной манере, ибо компилятор путается, что где. Ну что ж, поможем компилятору и разнесём типы Athlete и Club по разным модулям:

Теперь пишем:

Но капризный компилятор и этому варианту не будет рад, ибо хотя прежняя ошибка ушла, пришла новая:

Когда мы с вами глядим на эту строку:

нам предельно ясно, какое name имеется в виду, но компилятор не столь понятлив. Ладно, удовлетворим его каприз и уточним:

Теперь всё работает. Но такое решение тоже не торт. Рано или поздно количество модулей увеличится, и между alias-именами модулей могут возникнуть коллизии. Конечно, мы можем не использовать alias-имена и указывать имя модуля целиком:

но каждый раз уточнять “модульную принадлежность” наших полей - это скучновато.

Отказ от одноимённых полей

В конце концов, раз от них проблемы, так давайте же избавимся от них:

И далее пишем так:

Никаких коллизий, всё прекрасно работает, но это некрасивое решение. Ведь мы постоянно сами себя повторяем:

Читается как “масло масляное”. Нет, мы любим Haskell за его красоту, поэтому желаем чего-то лучшего.

Именной класс типов

Определим класс для работы с типами, имеющими имя:

Теперь создадим экземпляры этого класса для наших атлетов и клубов:

И теперь пишем:

Ну что ж, такое решение значительно красивее, ведь никакого “масла масляного” уже нет. Казалось бы, мы нашли идеальное решение, но…

Разные типы

Что произойдёт, если тип поля name у атлета станет другим, нежели у клуба? Допустим, мы решили использовать более продвинутый тип Text:

К сожалению, всё тут же сломается:

Вспомним класс типов HasName:

Методы этого класса дружат с обыкновенной строкой, а им тут подсовывают какой-то Text… Но врагу не сдаётся наш гордый Варяг! Сделаем класс более обобщённым:

Кстати, чтобы это работало, добавим необходимые Haskell-расширения в начале модуля Main.hs:

Теперь изменим экземпляры класса для наших типов:

Здесь мы учли разность типов имён атлетов и клубов. И теперь пишем:

Работает:

Это весьма хорошее решение, и мы могли бы на этом остановиться, но существует иной путь.

Линзы

Определим новый тип:

Перед нами - главная идея линз. Мы ведь хотим сделать работу с геттерами и сеттерами максимально изящной, не правда ли? Так вот идея в том, чтобы создать линзу (для русскоязычного читателя привычнее будет термин “лупа”), позволяющую нам сфокусироваться на каком-то значении. А фокусироваться нам нужно как раз для того, чтобы изменять (set) или получать (get).

Но чтобы всё это работало, мы должны определить линзы для каждого из наших типов:

Мы определили линзу athleteNameLens для фокусировки на имени атлета, а также линзу clubNameLens для фокусировки на имени клуба. Фактически, get и set - это лямбда-обёртки для работы с уже известным нам полем athleteName.

Возможно, вы спросите, зачем нужны такие сложности? Но взгляните на новое определение экземпляров класса HasName:

Всё сильно упростилось. Класс HasName теперь содержит одно-единственное “линзовое” значение name. Соответственно, в каждом экземпляре этого класса мы просто заявляем: “Значение name теперь связано с конкретной линзой, определённой для данного типа.”

И вот теперь наш главный код приобретает следующий вид:

Элегантно, не правда ли? Читается теперь как поэма:

Здесь set фокусируется на имени клуба через линзу name. Мы как будто говорим: “Измени то, на что смотрит эта линза”, или “Получи то, на что смотрит эта линза”. Грубо и схематично это можно представить себе так:

действие -> линза -> нечто, к чему применяется действие

Правда

А правда, друзья мои, состоит в том, что всё вышеизложенное есть не более чем мааааааленькая демонстрация. Существует мощный пакет lens, в котором всё уже реализовано очень умными людьми. К рассмотрению возможностей этого пакета мы приступим в следующих статьях.