линзы, fclabels, data-accessor - какая библиотека для доступа к структуре и мутации лучше - PullRequest
170 голосов
/ 24 апреля 2011

Существует как минимум три популярные библиотеки для доступа к полям записей и управления ими. Те, о которых я знаю, это: средство доступа к данным, флейбелы и линзы.

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

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

1 Ответ

196 голосов
/ 24 апреля 2011

Есть как минимум 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 можно напрямую перевести на вышеуказанный тип.Он предоставляет экземпляр Category для (:->), который полезен, поскольку позволяет создавать линзы.Он также предоставляет беззаконный тип Point, который обобщает понятие линзы, используемой здесь, и некоторую сантехнику для работы с изоморфизмами.

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

1048 * данные сбруи 1052 * [Изменить: data-accessor больше не использует это представление, но перешел в форму, аналогичную форме data-lens.Я держу этот комментарий, однако.]

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

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

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

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

Тем не менее, Хеннинг включил систему шаблонов-haskell для автоматического создания средств доступа для вас в отдельной ' data-accessor-template 'package.

Преимуществом этого является достаточно большой набор пакетов, которые уже используют его, например, 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 в основном пакете это моя наименее любимая реализация.

Данные линзы

[Редактировать: Начиная с версии 1.8.0, они перешли от пакета comonad-transformers к линзе данных]

Мой пакет 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))

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

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

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

Этот подход был впервые отмечен и описан в Расселе О'Конноре Functor для Lens, как Applicative для Biplate: введение Multiplate и был в блогах, основанных на Препринт Джереми Гиббонс.

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

Таким образом, линзы в data-lens образуют Category (в отличие от пакета lenses), являются Haskell 98 (в отличие от fclabels / lenses), являются нормальными (в отличие от задней части data-accessor) и обеспечивают чуть более эффективную реализацию, data-lens-fd предоставляет функциональные возможности для работы с MonadState для тех, кто хочет выйти за пределы Haskell 98, а механизм template-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))

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

линзы van Laarhoven

Мы можем моделировать линзы так, чтобы они могли быть составлены с (.) и 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» и «б». Это значительно снижает стоимость усыновления.

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

...