镜头,fclabels,data-accessor - 哪个用于结构访问和修改的库更好?

178

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

个人而言,我开始使用 data-accessor 并一直在使用它们。然而最近在 haskell-cafe 上出现了 fclabels 更优秀的观点。

因此,我对这三个(或更多)库进行比较感兴趣。


3
截至今天,lens包具有最丰富的功能和文档,因此如果您不介意其复杂性和依赖关系,这是一种可行的选择。 - modular
1个回答

202

我知道至少有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 扩展。

data-accessor

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

data-accessorfclabels更受欢迎,部分原因是它符合Haskell 98标准。然而,它选择的内部表示让我有点不舒服。

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

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

因此,为了获取镜头的值,您必须提交一个未定义的值作为'a'参数!这让我觉得实现非常丑陋和临时性。

话虽如此,Henning已经在单独的data-accessor-template包中包含了template-haskell管道,以自动生成访问器。

它有一个相当大的套件集,已经使用了Haskell 98,并提供了至关重要的Category实例,因此,如果您不关心香肠是如何制作的,这个包实际上是一个相当合理的选择。

镜头

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

如果它确实为其镜头提供类型,它们将具有类似于rank-2的类型:

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

因此,我不太喜欢这种方法,因为它会无端地将您带出Haskell 98(如果您想要在抽象中提供类型),并且剥夺了镜头的类别实例,这会使您能够使用“。”将它们组合。该实现还需要多参数类型类。
请注意,这里提到的所有其他透镜库都提供了一些组合器,或者可以用于提供相同的状态聚焦效果,因此通过直接编码透镜并没有任何收益。
此外,开头所述的附加条件在这种形式下真的没有一个好的表达方式。与“fclabels”一样,这确实提供了一个模板-哈斯克尔方法,以便在主包中直接为记录类型自动生成透镜。
由于缺少类别实例、巴洛克编码和需要在主包中使用模板哈斯克尔,这是我最不喜欢的实现。
我的Store共同子类提供了基于data-lens包的透镜。
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))

你可以将其视为从getter和setter中提取公共参数,返回一个由检索元素结果和设置新值的setter组成的对。这提供了计算上的好处,因为这里的'setter'可以回收一些用于获取值的工作,使得'modify'操作比fclabels定义中更有效,特别是当访问器被链接时。
此表示还有一个很好的理论证明,因为满足本响应开头所述的3个定律的'Lens'值子集恰好是该包装函数为存储共代数的'comonad coalgebra'的那些镜头。这将镜头l的3个复杂法则转化为2个漂亮的无点等式:
extract . l = id
duplicate . l = fmap l . l

这种方法最初是由Russell O'Connor在他的 Functor is to Lens as Applicative is to Biplate: Introducing Multiplate 中注意到和描述的,基于预印本,Jeremy Gibbons进行了博客记录
它还包括一些用于严格处理镜头的组合器以及用于容器的一些常规镜头,例如Data.Map
因此,data-lens中的镜头形成一个Category(不像lenses软件包),是Haskell 98(不像fclabels/lenses),是合理的(不像data-accessor后端)并提供了稍微更有效的实现,data-lens-fd为那些愿意走出Haskell 98的人提供了与MonadState一起使用的功能,而模板哈斯克尔机制现在可以通过data-lens-template获得。

更新6/28/2012:其他Lens实现策略

同构透镜

有两种其他值得考虑的镜头编码。第一种提供了一种很好的理论方式来将镜头视为将结构分解为字段值和“其他所有内容”的方式。

给定等同类型

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

这样有效的成员满足hither . yon = idyon . 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

你可以自行验证函数组合是镜头组合。

我最近写过一篇文章,介绍如何进一步generalize van Laarhoven镜头,获取可以更改字段类型的镜头系列,只需将此签名泛化即可。

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

这样做的不幸后果是,谈论镜头的最佳方式是使用二阶多态,但在定义镜头时您无需直接使用该签名。
我上面为_2定义的Lens实际上是一个LensFamily
_2 :: Functor f => (a -> f b) -> (c,a) -> f (c, b)

我写了一个库,其中包括镜头、镜头族和其他泛化工具,包括获取器、设置器、折叠和遍历。它可以在hackage上作为lens软件包使用。

再次强调,这种方法的一个重要优势是,库维护人员可以在不产生任何镜头库依赖的情况下,在他们的库中以这种风格创建镜头,只需提供类型为Functor f => (b -> f b) -> a -> f a的函数,适用于他们特定的类型'a'和'b'。这大大降低了采用成本。

由于您不需要实际使用该软件包来定义新的镜头,因此它减轻了我早期关于保持Haskell 98的担忧。


29
我喜欢fclabels的乐观态度:-> - Tener
3
这两篇文章 Inessential Guide to data-accessorInessential guide to fclabels 可能值得注意。 - hvr
10
使用Haskell 1998的兼容性重要吗?因为这使得编译器的开发更加容易?我们不应该转而谈论Haskell 2010吗? - yairchu
56
哦不!我是“data-accessor”的原始作者,后来把它交给了Henning并停止关注。 a->r->(a,r)的表达方式也让我感到不舒服,我的原始实现就像你的“Lens”类型一样。Heeennnninngg!! - luqui
5
Yairchu 的意思是,这样做主要是为了让你的库能够在 GHC 以外的编译器上工作。没有其他人会使用 Template Haskell。2010 年的情况与此无关。 - Edward Kmett
显示剩余12条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接