178

至少有三个流行的库用于访问和操作记录字段。我知道的有:数据访问器、fclabels 和镜头。

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

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

4

1 回答 1

201

我知道至少有 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可能是最容易推理的镜头库,因为它a :-> b可以直接转换为上述类型。它提供了一个有用的类别实例,(:->)因为它允许您组合镜头。它还提供了一种无法无天的Point类型,它概括了这里使用的镜头的概念,以及一些用于处理同构的管道。

采用的一个障碍fclabels是主包包含模板-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获得镜头的价值,您必须为“a”参数提交一个未定义的值!这让我觉得这是一个非常丑陋和临时的实现。

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

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

镜片

接下来是lens包,它通过将 lens 直接定义为这样的 monad 同态,观察到一个 lens 可以提供两个 state monad 之间的 state monad 同态。

如果它真的费心为其镜头提供一种类型,那么它们将具有 rank-2 类型,例如:

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

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

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

此外,开头陈述的附带条件在这种形式中并没有很好的表达方式。与“fclabels”一样,它确实提供了模板-haskell 方法,用于直接在主包中为记录类型自动生成镜头。

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

数据镜头

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

我的包裹根据Store comonaddata-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 个定律的“镜头”值的子集正是那些包裹函数是存储共单数的“共单数代数”的镜头. 这将镜头的 3 个毛茸茸的定律转换l为 2 个很好的无点等效物:

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

这种方法首先在 Russell O'Connor 的Functoris to Lensas Applicativeis to中被注意到和描述Biplate:Introducing Multiplate基于Jeremy Gibbons 的预印本在博客中发表。

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

data-lens因此,形式 a的镜头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 = id,并且yon . hither = id

我们可以用以下方式表示镜头:

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

这些主要用作思考镜头含义的一种方式,我们可以将它们用作解释其他镜头的推理工具。

范拉霍文镜头

我们可以对镜头进行建模,使它们可以用(.)和组成id,即使没有Category实例,也可以使用

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

作为我们镜头的类型。

然后定义一个镜头很简单:

_2 f (a,b) = (,) a <$> f b

您可以自己验证功能组合是镜头组合。

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

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)

我编写了一个库,其中包含镜头、镜头系列和其他概括,包括 getter、setter、折叠和遍历。它可以作为 lens包在 hackage 上使用。

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

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

于 2011-04-24T07:31:21.233 回答