至少有三种流行的库可用于访问和操作记录字段。我所知道的有:data-accessor、fclabels 和 lenses。
个人而言,我开始使用 data-accessor 并一直在使用它们。然而最近在 haskell-cafe 上出现了 fclabels 更优秀的观点。
因此,我对这三个(或更多)库进行比较感兴趣。
至少有三种流行的库可用于访问和操作记录字段。我所知道的有:data-accessor、fclabels 和 lenses。
个人而言,我开始使用 data-accessor 并一直在使用它们。然而最近在 haskell-cafe 上出现了 fclabels 更优秀的观点。
因此,我对这三个(或更多)库进行比较感兴趣。
我知道至少有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-accessor比fclabels
更受欢迎,部分原因是它符合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)
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
定义中更有效,特别是当访问器被链接时。l
的3个复杂法则转化为2个漂亮的无点等式:extract . l = id
duplicate . l = fmap l . l
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 = 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
你可以自行验证函数组合是镜头组合。
我最近写过一篇文章,介绍如何进一步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的担忧。
:->
。 - Tenera->r->(a,r)
的表达方式也让我感到不舒服,我的原始实现就像你的“Lens”类型一样。Heeennnninngg!! - luqui
lens
包具有最丰富的功能和文档,因此如果您不介意其复杂性和依赖关系,这是一种可行的选择。 - modular