Lenses、fclabels、data-accessor - 哪个用于结构访问和突变的库更好

发布于 2024-11-02 18:47:49 字数 173 浏览 5 评论 0 原文

至少有三个流行的库用于访问和操作记录字段。我所知道的有:data-accessor、fclabels 和 Lenses。

就我个人而言,我从数据访问器开始,现在正在使用它们。然而最近在 haskell-cafe 上有一种观点认为 fclabels 更优越。

因此我对这三个(也许更多)库的比较感兴趣。

There are at least three popular libraries for accessing and manipulating fields of records. The ones I know of are: data-accessor, fclabels and lenses.

Personally I started with data-accessor and I'm using them now. However recently on haskell-cafe there was an opinion of fclabels being superior.

Therefore I'm interested in comparison of those three (and maybe more) libraries.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

魂ガ小子 2024-11-09 18:47:49

据我所知,至少有 4 个图书馆提供镜头。

透镜的概念是,它提供了与

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

提供两个功能同构的东西:一个 getter 和一个 setter,

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 的一个障碍是主包包含 template-haskell 管道,因此该包不是 Haskell 98,并且它还需要(相当无争议的)TypeOperators< /代码> 扩展名。

data-accessor

[编辑:data-accessor 不再使用此表示形式,但已转移到与 data-lens 类似的形式。不过,我保留了这个评论。]

data-accessorfclabels,部分原因是它 Haskell 98。然而,它对内部表示的选择让我有点呕吐。

它用来表示镜头的类型T在内部定义为:

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

因此,为了获取镜头的值,您必须为“a”提交一个未定义的值争论!在我看来,这是一个极其丑陋且临时的实现。

也就是说,Henning 已经包含了 template-haskell 管道,可以在单独的 '数据访问器模板' 包。

它的好处是已经有大量的软件包使用它,比如 Haskell 98,并提供了最重要的 Category 实例,所以如果你不注意香肠是如何制作的,这个套餐实际上是相当合理的选择。

镜头

接下来,有lens 包,它观察到,通过直接将lens定义为这样的单子同态,lens可以在两个状态单子之间提供状态单子同态。

如果它真的费心为其镜头提供一种类型,他们会有一个 2 级类型,例如:

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

结果,我宁愿不喜欢这种方法,因为它不必要地将您从 Haskell 98 中拉出来(如果您想要一种类型)抽象地提供给您的镜头)并剥夺您镜头的 Category 实例,这将允许您使用 . 组合它们。该实现还需要多参数类型类。

请注意,此处提到的所有其他镜头库都提供一些组合器,或者可用于提供相同的状态聚焦效果,因此直接以这种方式编码镜头不会获得任何效果。

此外,在这种形式中,开头所述的附加条件实际上并没有很好的表达。与“fclabels”一样,这确实提供了模板哈斯克尔方法,用于直接在主包中自动生成记录类型的镜头。

由于缺少 Category 实例、巴洛克编码以及主包中 template-haskell 的要求,这是我最不喜欢的实现。

data-lens

[编辑:从 1.8.0 开始,这些已从 comonad-transformers 包转移到 data-lens]

我的 data-lens 包根据 存储 comonad。

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

其中

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

Expanded this 相当于

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

您可以将其视为从 getter 和 setter 中分解出公共参数,以返回一个由检索元素的结果组成的对,以及一个用于放回新值的 setter。这提供了计算这样做的好处是,这里的“setter”可以回收一些用于获取值的工作,从而实现比 fclabels 定义更有效的“修改”操作,尤其是当访问器链接时。

这种表示法还有一个很好的理论依据,因为满足此响应开头所述的 3 个定律的“Lens”值的子集正是那些包装函数是存储 comonad 的“comonad 余代数”的透镜。这将镜头 l 的 3 个毛茸茸的定律转换为 2 个完美的无点等价物:

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

这种方法首先在 Russell O'Connor 的 Functor 之于 Lens 正如 Applicative 之于 Biplate:介绍 Multiplate< /a> 并且是 基于杰里米·吉本斯 (Jeremy Gibbons) 的预印本撰写的博客。

它还包括许多用于严格使用镜头的组合器和一些用于容器的库存镜头,例如Data.Map

因此,data-lens 中的镜头形成了一个 Category(与 lenses 包不同),是 Haskell 98(与 fclabels 不同) >/lenses),是理智的(与data-accessor的后端不同),并提供稍微更高效的实现,data-lens-fd 为那些愿意走出 Haskell 的人提供使用 MonadState 的功能98,并且 template-haskell 机制现在可以通过 data-lens-template 获得

2012 年 6 月 28 日更新:其他镜头实现策略

同构镜头

还有另外两种镜头编码值得考虑。第一个给出了一种很好的理论方法,将镜头视为将结构分解为领域价值和“其他一切”的方法。

给定一个同构类型

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 <
gt; f b

并且您可以自己验证函数组合就是透镜组合。

我最近写了一篇关于如何进一步推广 van Laarhoven 镜头以获得镜头的文章可以更改字段类型的系列,只需将此签名概括为

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

这确实会产生不幸的后果,即讨论镜头的最佳方式是使用 2 级多态性,但在定义镜头时不需要直接使用该签名。

我上面为 _2 定义的 Lens 实际上是一个 LensFamily

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

我编写了一个库,其中包括镜头、镜头系列和其他概括,包括 getter、setter、折叠和遍历。它可以在 hackage 上以 lens 包的形式提供。

同样,这种方法的一大优点是,库维护人员实际上可以在库中创建这种风格的镜头,而不会产生任何镜头库依赖性,只需提供类型为 Functor f => 的函数即可。 (b→fb)→一个-> f a,用于其特定类型“a”和“b”。这大大降低了采用成本。

由于您不需要实际使用该包来定义新镜头,因此我之前担心保留 Haskell 98 库的担忧减轻了很多。

There are at least 4 libraries that I am aware of providing lenses.

The notion of a lens is that it provides something isomorphic to

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

providing two functions: a getter, and a setter

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

subject to three laws:

First, that if you put something, you can get it back out

get l (put l b a) = b 

Second that getting and then setting doesn't change the answer

put l (get l a) a = a

And third, putting twice is the same as putting once, or rather, that the second put wins.

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

Note, that the type system isn't sufficient to check these laws for you, so you need to ensure them yourself no matter what lens implementation you use.

Many of these libraries also provide a bunch of extra combinators on top, and usually some form of template haskell machinery to automatically generate lenses for the fields of simple record types.

With that in mind, we can turn to the different implementations:

Implementations

fclabels

fclabels is perhaps the most easily reasoned about of the lens libraries, because its a :-> b can be directly translated to the above type. It provides a Category instance for (:->) which is useful as it allows you to compose lenses. It also provides a lawless Point type which generalizes the notion of a lens used here, and some plumbing for dealing with isomorphisms.

One hindrance to the adoption of fclabels is that the main package includes the template-haskell plumbing, so the package is not Haskell 98, and it also requires the (fairly non-controversial) TypeOperators extension.

data-accessor

[Edit: data-accessor is no longer using this representation, but has moved to a form similar to that of data-lens. I'm keeping this commentary, though.]

data-accessor is somewhat more popular than fclabels, in part because it is Haskell 98. However, its choice of internal representation makes me throw up in my mouth a little bit.

The type T it uses to represent a lens is internally defined as

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

Consequently, in order to get the value of a lens, you must submit an undefined value for the 'a' argument! This strikes me as an incredibly ugly and ad hoc implementation.

That said, Henning has included the template-haskell plumbing to automatically generate the accessors for you in a separate 'data-accessor-template' package.

It has the benefit of a decently large set of packages that already employ it, being Haskell 98, and providing the all-important Category instance, so if you don't pay attention to how the sausage is made, this package is actually pretty reasonable choice.

lenses

Next, there is the lenses package, which observes that a lens can provide a state monad homomorphism between two state monads, by definining lenses directly as such monad homomorphisms.

If it actually bothered to provide a type for its lenses, they would have a rank-2 type like:

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

As a result, I rather don't like this approach, as it needlessly yanks you out of Haskell 98 (if you want a type to provide to your lenses in the abstract) and deprives you of the Category instance for lenses, which would let you compose them with .. The implementation also requires multi-parameter type classes.

Note, all of the other lens libraries mentioned here provide some combinator or can be used to provide this same state focalization effect, so nothing is gained by encoding your lens directly in this fashion.

Furthermore, the side-conditions stated at the start don't really have a nice expression in this form. As with 'fclabels' this does provide template-haskell method for automatically generating lenses for a record type directly in the main package.

Because of the lack of Category instance, the baroque encoding, and the requirement of template-haskell in the main package, this is my least favorite implementation.

data-lens

[Edit: As of 1.8.0, these have moved from the comonad-transformers package to data-lens]

My data-lens package provides lenses in terms of the Store comonad.

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

where

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

Expanded this is equivalent to

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

You can view this as factoring out the common argument from the getter and the setter to return a pair consisting of the result of retrieving the element, and a setter to put a new value back in. This offers the computational benefit that the 'setter' here can recycle some of the work used to get the value out, making for a more efficient 'modify' operation than in the fclabels definition, especially when accessors are chained.

There is also a nice theoretical justification for this representation, because the subset of 'Lens' values that satisfy the 3 laws stated in the beginning of this response are precisely those lenses for which the wrapped function is a 'comonad coalgebra' for the store comonad. This transforms 3 hairy laws for a lens l down to 2 nicely pointfree equivalents:

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

This approach was first noted and described in Russell O'Connor's Functor is to Lens as Applicative is to Biplate: Introducing Multiplate and was blogged about based on a preprint by Jeremy Gibbons.

It also includes a number of combinators for working with lenses strictly and some stock lenses for containers, such as Data.Map.

So the lenses in data-lens form a Category (unlike the lenses package), are Haskell 98 (unlike fclabels/lenses), are sane (unlike the back end of data-accessor) and provide a slightly more efficient implementation, data-lens-fd provides the functionality for working with MonadState for those willing to step outside of Haskell 98, and the template-haskell machinery is now available via data-lens-template.

Update 6/28/2012: Other Lens Implementation Strategies

Isomorphism Lenses

There are two other lens encodings worth considering. The first gives a nice theoretical way to view a lens as a way to break a structure into the value of the field, and 'everything else'.

Given a type for isomorphisms

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

such that valid members satisfy hither . yon = id, and yon . hither = id

We can represent a lens with:

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

These are primarily useful as a way to think about the meaning of lenses, and we can use them as a reasoning tool to explain other lenses.

van Laarhoven Lenses

We can model lenses such that they can be composed with (.) and id, even without a Category instance by using

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

as the type for our lenses.

Then defining a lens is as easy as:

_2 f (a,b) = (,) a <
gt; f b

and you can validate for yourself that function composition is lens composition.

I've recently written on how you can further generalize van Laarhoven lenses to get lens families that can change the types of fields, just by generalizing this signature to

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

This does have the unfortunate consequence that the best way to talk about lenses is to use rank 2 polymorphism, but you don't need to use that signature directly when defining lenses.

The Lens I defined above for _2 is actually a LensFamily.

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

I've written a library that includes lenses, lens families, and other generalizations including getters, setters, folds and traversals. It is available on hackage as the lens package.

Again, a big advantage of this approach is that library maintainers can actually create lenses in this style in your libraries without incurring any lens library dependency whatsoever, by just supplying functions with type Functor f => (b -> f b) -> a -> f a, for their particular types 'a' and 'b'. This greatly lowers the cost of adoption.

Since you don't need to actually use the package to define new lenses, it takes a lot of pressure off my earlier concerns about keeping the library Haskell 98.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文