镜头,fclabels,数据访问器 - 用于结构访问和变异的库更好

Ten*_*ner 170 haskell record data-structures lenses

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

我个人开始使用数据访问器,现在我正在使用它们.然而最近在haskell-cafe上有一个fclabels优越的意见.

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

Edw*_*ETT 196

我知道至少有4个库提供镜头.

镜头的概念是它提供了同构的东西

data Lens a b = Lens (a -> b) (b -> a -> a)
Run Code Online (Sandbox Code Playgroud)

提供两个功能:一个getter和一个setter

get (Lens g _) = g
put (Lens _ s) = s
Run Code Online (Sandbox Code Playgroud)

受三项法律约束:

首先,如果你放了东西,你可以把它拿回来

get l (put l b a) = b 
Run Code Online (Sandbox Code Playgroud)

其次,获取然后设置不会改变答案

put l (get l a) a = a
Run Code Online (Sandbox Code Playgroud)

第三,两次投注与投注一次相同,或者更确切地说,第二次投注获胜.

put l b1 (put l b2 a) = put l b1 a
Run Code Online (Sandbox Code Playgroud)

请注意,类型系统不足以为您检查这些法律,因此无论您使用何种镜头实现,都需要自己确保它们.

这些库中的许多还在顶部提供了一组额外的组合器,通常还有某种形式的模板haskell机器,可以自动为简单记录类型的领域生成镜头.

考虑到这一点,我们可以转向不同的实现:

实现

fclabels

fclabels可能是最容易推理的镜头库,因为它a :-> b可以直接翻译成上述类型.它提供了一个有用的类别实例,(:->)因为它允许你组成镜头.它还提供了一种无法无天的Point类型,它概括了这里使用的镜头的概念,以及一些用于处理同构的管道.

采用的一个障碍fclabels是主包包含模板 - haskell管道,因此包不是Haskell 98,它还需要(相当无争议的)TypeOperators扩展.

数据访问

[编辑:data-accessor不再使用此表示形式,但已移至类似于该表格的形式data-lens.不过,我保留了这篇评论.

数据访问器比某些程度上更受欢迎fclabels,部分原因因为它 Haskell 98.但是,它对内部表示的选择让我有点呕吐.

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

newtype T r a = Cons { decons :: a -> r -> (a, r) }
Run Code Online (Sandbox Code Playgroud)

因此,为了get镜头的价值,你必须为'a'参数提交一个未定义的值!这让我觉得这是一个令人难以置信的丑陋和临时实现.

也就是说,Henning已经包含了模板-haskell管道,可以在一个单独的" data-accessor-template "包中自动生成访问.

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

镜头

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

如果它真的打算为它的镜片提供一种类型,它们将具有等级2类型,如:

newtype Lens s t = Lens (forall a. State t a -> State s a)
Run Code Online (Sandbox Code Playgroud)

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

注意,这里提到的所有其他镜头库都提供了一些组合器,或者可以用来提供相同的状态聚焦效果,因此通过这种方式直接编码镜头无法获得任何结果.

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

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

数据透镜

[编辑:从1.8.0开始,这些已经从comonad-transformers包转移到数据镜头]

我的data-lens包装提供了商店 comonad的镜头.

newtype Lens a b = Lens (a -> Store b a)
Run Code Online (Sandbox Code Playgroud)

哪里

data Store b a = Store (b -> a) b
Run Code Online (Sandbox Code Playgroud)

扩展这相当于

newtype Lens a b = Lens (a -> (b, b -> a))
Run Code Online (Sandbox Code Playgroud)

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

对于这种表示也有一个很好的理论上的理由,因为满足在这个响应开始时所述的3个定律的"镜头"值的子集恰好是那些包装函数是商店comonad的"comonad代数"的镜头. .这将镜头的3个毛状定律转换l为2个完美无效的等效物:

extract . l = id
duplicate . l = fmap l . l
Run Code Online (Sandbox Code Playgroud)

这种方法是首先指出和罗素·奥康纳的描述FunctorLensApplicativeBiplate:引入多板,并根据预印本的博客上讲述由杰里米·吉本斯.

它还包括许多用于严格使用镜头的组合器和一些用于容器的镜片,例如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 }
Run Code Online (Sandbox Code Playgroud)

使有效成员满意hither . yon = id,并且yon . hither = id

我们可以代表一个镜头:

data Lens a b = forall c. Lens (Iso a (b,c))
Run Code Online (Sandbox Code Playgroud)

这些主要用作考虑镜头含义的方法,我们可以将它们用作解释其他镜头的推理工具.

van Laarhoven镜头

我们可以模拟镜头,使得它们可以与组成(.)id,即使没有Category使用实例

type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a
Run Code Online (Sandbox Code Playgroud)

作为我们镜头的类型.

然后定义镜头就像:

_2 f (a,b) = (,) a <$> f b
Run Code Online (Sandbox Code Playgroud)

你可以自己验证功能组成是镜片组成.

我最近写过如何进一步推广van Laarhoven镜头以获得可以改变字段类型的镜头系列,只需将此签名推广到

type LensFamily a b c d = forall f. Functor f => (c -> f d) -> a -> f b
Run Code Online (Sandbox Code Playgroud)

这确实有一个令人遗憾的结果,谈论镜头的最佳方法是使用2级多态,但在定义镜头时不需要直接使用该签名.

Lens上面定义的我_2其实是一个LensFamily.

_2 :: Functor f => (a -> f b) -> (c,a) -> f (c, b)
Run Code Online (Sandbox Code Playgroud)

我写过一个包含镜头,镜头系列和其他概括的库,包括getter,setter,folds和traversal.它可作为lens包装在hackage上提供 .

同样,这种方法的一大优点是图书馆维护者实际上可以在你的图书馆中创建这种风格的镜头而不会产生任何镜头库依赖性,只需提供类型的功能Functor f => (b -> f b) -> a -> f a,因为他们的特定类型'a'和'b'.这大大降低了采用的成本.

由于您不需要实际使用该软件包来定义新镜头,因此我需要承担很多关于保留库Haskell 98的担忧.

  • 不好了!我是`data-accessor`的原作者,然后我将它传递给Henning并停止关注.`a - > r - >(a,r)`表示也让我感到不舒服,而我原来的实现就像你的'镜头'类型.Heeennnninngg! (55认同)
  • 我喜欢fclabels的乐观方法`: - >` (28认同)
  • Haskell 1998兼容重要吗?因为它使编译器开发更容易?我们不应该转而谈论Haskell 2010吗? (10认同)
  • Yairchu:主要是因为你的图书馆可能有机会使用除ghc以外的编译器.没有其他人*拥有*模板Haskell.2010年没有添加任何相关内容. (5认同)
  • 文章[数据访问者的非必然指南](http://blog.ezyang.com/2010/04/inessential-guide-to-data-accessor/)和[关于fclabels的非必要指南](http:// blog. ezyang.com/2010/04/inessential-guide-to-fclabels/)可能值得注意 (3认同)
  • `fclabels`具有类型`Point`的原因是提供与`Applicative`实例的并行组合,紧接着`Lens`的`Category`实例提供的顺序组合. (3认同)
  • 在我看来,有关`data-accessor`的信息不再准确:因为0.2.1.8它使用[`newtype T ra = Cons {decons :: r - >(a,a - > r)}`](http ://hackage.haskell.org/packages/archive/data-accessor/0.2.2.2/doc/html/src/Data-Accessor-Private.html#T). (3认同)