линзы, fclabels, data-accessor - какая библиотека для доступа к структуре и мутации лучше

Существует как минимум три популярных библиотеки для доступа к полям записей и управления ими. Я знаю такие: data-accessor, fclabels и линзы.

Лично я начал с средств доступа к данным и использую их сейчас. Однако недавно о haskell-cafe было мнение, что fclabels лучше.

Поэтому мне интересно сравнить эти три (а может и больше) библиотек.


person Tener    schedule 23.04.2011    source источник
comment
На сегодняшний день пакет lens имеет самые богатые функциональные возможности и документацию, поэтому, если вы не против его сложность и зависимости, это правильный путь.   -  person modular    schedule 05.10.2016


Ответы (1)


Я знаю как минимум 4 библиотеки, которые предоставляют линзы.

Понятие линзы состоит в том, что она обеспечивает нечто изоморфное

data Lens a b = Lens (a -> b) (b -> a -> a)

предоставляет две функции: геттер и сеттер

get (Lens g _) = g
put (Lens _ s) = s

подчиняется трем законам:

Во-первых, если вы что-то положите, вы можете вернуть это обратно

get l (put l b a) = b 

Во-вторых, получение, а затем настройка не меняют ответа

put l (get l a) a = a

И, в-третьих, ставить дважды - это то же самое, что ставить один раз, или, точнее, выигрывает вторая ставка.

put l b1 (put l b2 a) = put l b1 a

Обратите внимание, что системы типов недостаточно для проверки этих законов за вас, поэтому вам необходимо обеспечить их самостоятельно, независимо от того, какую реализацию линзы вы используете.

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

Имея это в виду, мы можем перейти к различным реализациям:

Реализации

fclabels

fclabels, пожалуй, самая популярная из библиотек объективов, поскольку ее a :-> b можно напрямую перевести на вышеуказанный тип. Он предоставляет Категория экземпляр для (:->), который полезен, так как позволяет составлять линзы. Он также предоставляет беззаконный Point тип, который обобщает понятие линзы, используемой здесь, и некоторые способы работы с изоморфизмами.

Одним из препятствий для принятия fclabels является то, что основной пакет включает в себя соединение шаблон-haskell, поэтому пакет не является Haskell 98, и он также требует (довольно не спорного) расширения TypeOperators.

аксессор данных

[Изменить: data-accessor больше не использует это представление, но переместился в форму, аналогичную форме data-lens. Однако я сохраняю этот комментарий.]

data-accessor несколько более популярен, чем fclabels, отчасти потому, что он < / em> Haskell 98. Однако его выбор внутреннего представления заставляет меня немного рвать во рту.

Тип T, который он использует для представления линзы, внутренне определяется как

newtype T r a = Cons { decons :: a -> r -> (a, r) }

Следовательно, чтобы get значение линзы, вы должны передать неопределенное значение для аргумента 'a'! Это кажется мне невероятно уродливой и специальной реализацией.

Тем не менее, Хеннинг включил сантехнику template-haskell для автоматической генерации аксессуаров для вас в отдельный 'data-accessor-template '.

Он имеет преимущество в виде прилично большого набора пакетов, которые уже используют его, это Haskell 98 и предоставляет чрезвычайно важный экземпляр Category, поэтому, если вы не обращаете внимания на то, как делается колбаса, этот пакет на самом деле довольно разумный выбор.

линзы

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

Если бы он действительно позаботился о том, чтобы предоставить тип для своих линз, у них был бы тип ранга 2, например:

newtype Lens s t = Lens (forall a. State t a -> State s a)

В результате мне этот подход скорее не нравится, поскольку он без нужды вырывает вас из Haskell 98 (если вы хотите, чтобы тип предоставлялся вашим линзам в абстрактном виде) и лишает вас экземпляра Category для линз, что позволило бы вы составляете их с .. Для реализации также требуются классы с несколькими параметрами.

Обратите внимание, что все другие библиотеки линз, упомянутые здесь, предоставляют некоторый комбинатор или могут использоваться для обеспечения того же эффекта фокусировки состояния, так что ничего не получится от кодирования вашего объектива напрямую таким образом.

Более того, побочные условия, указанные в начале, действительно не имеют хорошего выражения в этой форме. Как и в случае с fclabels, он предоставляет метод template-haskell для автоматического создания линз для типа записи непосредственно в основном пакете.

Из-за отсутствия экземпляра Category, барочной кодировки и требования шаблона haskell в основном пакете это моя наименее любимая реализация.

data-lens

[Edit: Начиная с версии 1.8.0, они были перемещены из пакета comonad-transformers в data-lens]

Мой пакет data-lens предоставляет линзы с точки зрения Store comonad.

newtype Lens a b = Lens (a -> Store b a)

куда

data Store b a = Store (b -> a) b

В развернутом виде это эквивалентно

newtype Lens a b = Lens (a -> (b, b -> a))

Вы можете рассматривать это как выделение общего аргумента из получателя и установщика, чтобы вернуть пару, состоящую из результата извлечения элемента, и установщика, чтобы вернуть новое значение. Это дает вычислительное преимущество, которое имеет 'установщик' здесь можно повторно использовать часть работы, использованной для получения значения, что делает операцию «изменения» более эффективной, чем в определении fclabels, особенно когда аксессоры связаны.

Существует также хорошее теоретическое обоснование этого представления, потому что подмножество значений 'Lens', которые удовлетворяют трем законам, указанным в начале этого ответа, - это именно те линзы, для которых обернутая функция является 'коалгеброй комонад' для комонады магазина. . Это преобразует 3 сложных закона для объектива l в 2 приятных безоточечных эквивалента:

extract . l = id
duplicate . l = fmap l . l

Этот подход был впервые отмечен и описан в статье Рассела О'Коннора Functor is to Lens as Applicative is to Biplate: Introduction Multiplate < / a> и был в блоге about на основе препринта Джереми Гиббонса.

Он также включает ряд комбинаторов для работы только с линзами и некоторые стандартные линзы для контейнеров, например Data.Map.

Таким образом, линзы в data-lens образуют Category (в отличие от пакета lenses), относятся к Haskell 98 (в отличие от _37 _ / _ 38_), разумны (в отличие от задней части data-accessor) и обеспечивают немного более эффективную реализацию, _ 40_ предоставляет функциональные возможности для работы с MonadState для тех, кто хочет выйти за пределы Haskell 98, а также механизм шаблонов haskell теперь доступен через data-lens-template.

Обновление от 28.06.2012: другие стратегии внедрения объективов

Линзы изоморфизма

Стоит рассмотреть еще два варианта кодировки линз. Первый дает хороший теоретический способ рассматривать линзу как способ разбить структуру на значение поля и «все остальное».

Дан тип изоморфизмов

data Iso a b = Iso { hither :: a -> b, yon :: b -> a }

таким образом, чтобы действительные члены удовлетворяли hither . yon = id и yon . hither = id

Мы можем представить линзу с:

data Lens a b = forall c. Lens (Iso a (b,c))

Они в первую очередь полезны как способ осмыслить значение линз, и мы можем использовать их как инструмент рассуждения для объяснения других линз.

Линзы Ван Лаарховена

Мы можем моделировать линзы так, чтобы их можно было составить из (.) и id, даже без экземпляра Category, используя

type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a

как тип для наших линз.

Тогда определить линзу так же просто, как:

_2 f (a,b) = (,) a <$> f b

и вы можете убедиться, что функциональная композиция - это композиция линзы.

Недавно я писал о том, как можно дополнительно обобщить линзы Ван Лаарховена, чтобы получить семейства линз. которые могут изменять типы полей, просто обобщая эту сигнатуру на

type LensFamily a b c d = forall f. Functor f => (c -> f d) -> a -> f b

Это имеет неприятные последствия: лучший способ говорить о линзах - это использовать полиморфизм ранга 2, но вам не нужно использовать эту сигнатуру напрямую при определении линз.

Lens, который я определил выше для _2, на самом деле является LensFamily.

_2 :: Functor f => (a -> f b) -> (c,a) -> f (c, b)

Я написал библиотеку, которая включает линзы, семейства линз и другие обобщения, включая геттеры, сеттеры, складки и обходы. Он доступен для взлома в виде пакета lens.

Опять же, большим преимуществом этого подхода является то, что сопровождающие библиотеки могут фактически создавать линзы в этом стиле в ваших библиотеках без какой-либо зависимости библиотеки линз, просто предоставляя функции с типом Functor f => (b -> f b) -> a -> f a для их конкретных типов 'a' и 'b'. Это значительно снижает стоимость усыновления.

Поскольку на самом деле вам не нужно использовать пакет для определения новых линз, это снимает большую нагрузку с моих прежних опасений по поводу сохранения библиотеки Haskell 98.

person Community    schedule 24.04.2011
comment
очень хорошая запись! что вы думаете о библиотеке fields? - person hvr; 24.04.2011
comment
Я думаю, что это довольно тяжелое расширение для того, что он выполняет. знак равно - person Edward KMETT; 24.04.2011
comment
Мне нравится fclabels за оптимистичный подход :-> - person Tener; 24.04.2011
comment
Причина, по которой fclabels имеет тип Point, состоит в том, чтобы обеспечить параллельную композицию с ее Applicative экземпляром, рядом с последовательной композицией, которую предоставляет Category экземпляр Lens. - person Sjoerd Visscher; 24.04.2011
comment
@Sjoerd Visscher: Не поймите меня неправильно. Мне нравится Point. Это интересный тип. Также может быть полезно описывать такие вещи, как агрегаторы, которые добавляют число к счетчику и т. Д. Но нет никаких законов для рассуждений о том, что могло бы образовать действительную точку, а не действительную линзу, потому что получение и размещение стали не связанными друг с другом. Следовательно, «беззаконие». ;) Кстати, fmapL не генерирует действительные линзы по законам линз. Я понял это, когда добавил в свои линзы свойства scalacheck в scala. - person Edward KMETT; 24.04.2011
comment
Для fmapL рассмотрите возможность использования его для подъема любого старого Lens в Maybe Applicative. Когда вы вставляете Just foo в Nothing, результат остается Nothing, поэтому, когда вы собираетесь получить значение этого поля с помощью линзы, вы получаете Nothing, а не свой Just foo, и первый закон не работает. - person Edward KMETT; 24.04.2011
comment
@EdwardKmett Я просто указал на особенность fclabels, которая уникальна среди пакетов линз (afaik), и поэтому ее следует упомянуть при тщательном сравнении. Возможно, в документации следует упомянуть, что fmapL создает правильные линзы только с типами контейнеров с фиксированным количеством «отверстий», например Vec n, у которых есть zip-подобный экземпляр Applicative. Однако созданная линза является наиболее естественной из возможных для Maybe, в некотором расплывчатом смысле, который, вероятно, следует сделать более конкретным. - person Sjoerd Visscher; 24.04.2011
comment
Важна ли совместимость с Haskell 1998? Потому что это упрощает разработку компилятора? И не следует ли вместо этого перейти к разговору о Haskell 2010? - person yairchu; 24.04.2011
comment
О, нет! Я был первоначальным автором data-accessor, а затем передал его Хеннингу и перестал обращать внимание. Представление a -> r -> (a,r) также доставляет мне дискомфорт, и моя первоначальная реализация была точно такой же, как ваш тип Lens. Heeennnninngg !! - person luqui; 24.04.2011
comment
Яирчу: в основном это для того, чтобы ваша библиотека могла работать с другим компилятором, кроме ghc. Ни у кого другого нет шаблона Haskell. 2010 год не добавляет сюда ничего важного. - person Edward KMETT; 25.04.2011
comment
Шёрд: проблема Point в том, что получение и настройка не имеют ничего общего друг с другом. Я бы просто использовал аппликативный экземпляр для (- ›) e для составления геттеров, чем использовал бы аппликатив, который выполнял две несвязанные задачи: разветвление входов и составление выходов, потому что любая точка, построенная таким образом, не сможет быть в любом случае завернутый обратно в действующую линзу. - person Edward KMETT; 25.04.2011
comment
Шёрд: то, что вы ищете, - это сохранение ограничений. Этим свойством обладают представимые функторы, см. Пакет представимых функторов. Хотя определение для Maybe, данное fmapL, «настолько близко к объективу, насколько вы можете получить», оно все же не является объективом. ;) - person Edward KMETT; 25.04.2011
comment
@SjoerdVisscher: я добавил _ 1_ обеспечивает более безопасный fmapL для представимых функторов - person Edward KMETT; 28.04.2011
comment
Мне кажется, что информация о data-accessor больше не точна: начиная с версии 0.2.1.8 он использует _ 2_. - person Ben Millwood; 07.08.2012
comment
@EdwardKmett Я вижу опечатку в вашем определении _2. Разве это не должно быть _2 f (a,b) = (,) a <$> f b? - person Jason Dagit; 27.10.2012
comment
В качестве дополнения я хотел бы добавить, что тип линзы data-accessor был изменен на newtype T r a = Cons {decons :: r -> (a, a -> r)}, что примерно изоморфно тому, что используется в data-lens. - person Gregory Crosswhite; 07.02.2013