透镜,光标,数据访问器-哪个库的结构访问和变异是更好的

至少有三种流行的库用于访问和操作记录字段。我所知道的有: 数据访问器,标签和镜头。

就个人而言,我开始使用数据访问器,现在正在使用它们。然而最近在 Haskell 咖啡馆,有一种观点认为 Fclabels 更优越。

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

17541 次浏览

据我所知,至少有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-lens的形式。不过,我还是要保留这篇评论。]

Data-accessor fclabels要流行一些,部分原因是因为它是 Haskell 98。然而,它选择的内部表征让我有点想吐。

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

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

因此,为了 get的价值一个镜头,你必须提交一个未定义的值的’一’参数!这给我的印象是一个难以置信的丑陋和临时的实现。

也就是说,Henning 在单独的“ 数据访问器模板”包中包含了 template-haskell 管道,可以自动为您生成访问器。

它的好处在于已经使用了相当大的一组包,就是 Haskell 98,并且提供了至关重要的 Category实例,所以如果你不注意香肠是如何制作的,这个包实际上是相当合理的选择。

镜片

接下来是 镜片包,它观察到透镜可以提供两个状态单子之间的状态单子同态,通过直接定义透镜 作为这样的单子同态。

如果它真的愿意为它的镜头提供一种类型,它们会有一个排名第二的类型,比如:

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

因此,我宁愿不喜欢这种方法,因为它不必要地把你从 Haskell 98(如果你想要一个类型提供给你的镜头在抽象)和剥夺你的 Category实例的镜头,这将让你组成它们与 .。实现还需要多参数类型类。

注意,这里提到的所有其他镜头库都提供了一些组合器,或者可以用来提供相同的状态聚焦效果,所以以这种方式直接对镜头进行编码没有任何好处。

此外,在开始时声明的副条件在这种形式中并没有很好的表达。与“ fclabels”一样,它也提供了模板-哈斯克尔(template-haskell)方法,可以直接在主包中为记录类型自动生成镜头。

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

数据透镜

[编辑: 从1.8.0版本开始,它们已经从辅助变压器包转移到了数据镜头]

我的 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”可以回收一些用于获取值的工作,比在 fclabels定义中更有效的“修改”操作,特别是当访问器被链接时。

这种表示也有一个很好的理论依据,因为“镜头”值的子集满足这个响应的开头陈述的3个定律,正是那些镜头的包装函数是一个“共轭余代数”的商店共轭。这将镜头 l的3个毛状定律转化为2个不错的无点等价物:

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

这种方法第一次被注意到和描述在罗素奥康纳的 ABC0对于 ABC1就像 ABC2对于 Biplate: 引入多极板根据预印本在博客上发表的杰里米吉本斯。

它还包括一些严格工作的镜头组合器和一些股票镜头的容器,如 Data.Map

因此,data-lens中的镜头形成了一个 Category(不像 lenses包) ,是 Haskell 98(不像 fclabels/lenses) ,是健全的(不像 data-accessor的后端) ,并提供了一个稍微更有效的实现,data-lens-fd提供了与 MonadState 一起工作的功能,那些愿意走出 Haskell 98,和模板-Haskell 机器现在可以通过 data-lens-template

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

同构透镜

还有另外两种镜头编码值得考虑。第一个给出了一个很好的理论方法来观察一个透镜作为一种方法来打破一个结构进入场的价值,以及“其他一切”。

给定同构的一种类型

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

你可以自己验证复合函数是镜头构图。

我最近写了如何进一步 推广范拉尔霍芬透镜得到镜头系列,可以改变领域的类型,只是通过泛化这个签名

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)

我已经写了一个包括镜头,镜头族,和其他一般化包括吸引子,设置,折叠和遍历库。它作为 lens软件包在黑客攻击中可用。

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

因为你不需要真正使用软件包来定义新的镜头,它减轻了我早先关于保持图书馆 Haskell 98的压力。