镜头和部分镜头有什么区别?

时间:2015-08-26 10:15:47

标签: scala haskell scalaz lenses calmm

“镜头”和“部分镜头”在名称和概念上看起来相似。他们有什么不同?在什么情况下我需要使用其中一种?

标记Scala和Haskell,但我欢迎与任何具有镜头库的功能语言相关的解释。

3 个答案:

答案 0 :(得分:12)

根据Haskell lens命名法,描述部分镜片 - 我将称之为棱镜(除了它们不是!请参阅Ørjan的评论) - 我想首先拍摄一个不同看镜头本身。

镜头Lens s a表示给定s我们可以“关注”s类型的a子组件,查看它,替换它,以及(如果我们使用镜头系列变体Lens s t a b)甚至改变它的类型。

一种看待这种情况的方法是Lens s a见证s和元组类型(r, a)之间的同构,等价,对于某些未知类型{ {1}}。

r

这为我们提供了所需要的内容,因为我们可以将Lens s a ====== exists r . s ~ (r, a) 拉出来,替换它,然后通过等效向后运行以获得新的a而不更新s

现在让我们花一点时间通过代数数据类型刷新我们的高中代数。 ADT中的两个关键操作是乘法和求和。当我们的类型包含 aa * b的项目时,我们会写出a类型,当我们编写b时的类型包含 a + ba

的项目。

在Haskell中,我们将b写为a * b,即元组类型。我们将(a, b)写为a + b,两种类型。

产品将数据捆绑在一起,总和代表捆绑选项。产品可以表示只有一个你想要选择的东西(一次),而总和代表失败的想法,因为你希望采取一个选项(在左边一边说,但是不得不满足于另一个(沿着右边)。

最后,总和和产品是绝对双重的。他们融合在一起并拥有一个没有另一个,就像大多数PL一样,让你处于一个尴尬的地方。

那么让我们看一下当我们对照(部分)我们的镜片配方时会发生什么。

Either a b

这是exists r . s ~ (r + a) 类型s 其他内容a的声明。我们有一个类似r的东西,它体现了选择(和失败)的概念,深入到它的核心。

这正是一个棱镜(或部分镜头)

lens

那么这对一些简单的例子有何影响?

好吧,考虑一下“无视”清单的棱镜:

Prism s a ====== exists r . s ~ (r + a)
                 exists r . s ~ Either r a

它相当于这个

uncons :: Prism [a] (a, [a])

并且head :: exists r . [a] ~ (r + (a, [a])) 在这里需要的是相对明显的:完全失败,因为我们有一个空列表!

为了证实r类型,我们需要编写一种方法,将a ~ b转换为a,将b转换为b,以便它们相互颠倒。让我们写一下,以便通过神话功能描述我们的棱镜

a

这演示了如何使用这种等价(至少在原则上)来创建棱镜,并且还表明当我们使用列表之类的类似类型时,它们应该感觉非常自然。

答案 1 :(得分:9)

镜头是一种“功能参考”,允许您以更大的值提取和/或更新广义的“字段”。对于普通的非部分镜头,该字段总是需要那里,对于任何包含类型的值。如果您想要查看可能并非总是存在的“字段”之类的内容,则会出现问题。例如,在“列表的第n个元素”(如Scalaz文档@ChrisMartin粘贴中列出)的情况​​下,列表可能太短。

因此,“部分镜头”将镜头概括为场可能或可能不总是以较大值存在的情况。

Haskell lens库中至少有三件事你可以认为是“部分镜头”,其中没有一件完全符合Scala版本:

  • 普通Lens,其“字段”为Maybe类型。
  • A Prism,如@ J.Abrahamson所述。
  • A Traversal

它们都有它们的用途,但前两个太受限制而不包括所有情况,而Traversal s“太笼统”。在这三个中,只有Traversal支持“列表的第n个元素”示例。

  • 对于“Lens给出Maybe - 包裹值”版本,镜片定律会有所不同:要有合适的镜头,您应该能够将其设置为{{ 1}}删除可选字段,然后将其设置回原来的状态,然后返回相同的值。这适用于Nothing说(并且Control.Lens.At.atMap - 类似容器提供了这样的镜头),但不适用于列表,其中删除例如Map元素不能避免扰乱后者。

  • 从某种意义上说,0构造函数(大约是Scala中的case类)而不是字段的泛化。因此,它所提供的“字段”应该包含所有信息以重新生成整个结构(您可以使用Prism函数。)

  • review可以做“列表的第n个元素”就好了,事实上至少有两个不同的函数ixelement都适用于此(但与其他容器略有不同。)

感谢Traversal的类型类魔术,任何lensPrism自动作为Lens,而Traversal给出Lens通过与Maybe合成,可以将已包装的可选字段转换为普通可选字段的Traversal

但是,traverse在某种意义上一般,因为它不限于单个字段:Traversal可以任何“目标”字段的数量。 E.g。

Traversal

是一个elements odd ,它会愉快地浏览列表中所有奇数索引的元素,更新和/或从中提取所有信息。

理论上,您可以定义第四个变体(“仿射遍历”@ J.Abrahamson提及)我认为可能更接近于Scala的版本,但由于Traversal库本身之外的技术原因它们不适合图书馆的其他部分 - 您必须明确转换这样的“部分镜头”以使用它的一些lens操作。

此外,它不会比普通的Traversal更多地购买你,因为例如一个简单的运算符(^?),只提取遍历的第一个元素。

(据我所知,技术原因是定义“仿射遍历”所需的Pointed类型类不是Traversal的超类,普通{{1}使用。)

答案 2 :(得分:8)

Scalaz文档

以下是Scalaz LensFamilyPLensFamily的scaladocs,并强调了差异。

镜头:

  

A Lens Family,提供了一种纯粹的功能性方法,可以访问和检索 一个字段 ,从类型B1转换为B2类型记录同时从类型A1转换为类型A2 scalaz.Lens A1 =:= A2B1 =:= B2的便捷别名。

     

术语"字段"不应该被限制性地解释为一个类的成员。例如, 镜头系列可以解决Set 的成员资格。

部分镜头:

  

Partial Lens Families,提供了一种纯粹功能性的方法来访问和检索 可选字段 从类型B1转换为类型B2在同时从类型A1转换为类型A2的记录中。 scalaz.PLens A1 =:= A2B1 =:= B2的便捷别名。

     

术语"字段"不应该被限制性地解释为一个类的成员。例如, 部分镜头系列可以解决List 的第n个元素。

符号

对于那些不熟悉scalaz的人,我们应该指出符号类别别名:

type @>[A, B] = Lens[A, B]
type @?>[A, B] = PLens[A, B]

在中缀表示法中,这意味着从类型B的记录中检索A类型字段的镜头类型表示为A @> B,而部分镜头表示为A @?> B {1}}。

淘金者

Argonaut(一个JSON库)提供了很多部分镜头的例子,因为JSON的无模式特性意味着尝试从任意JSON值检索某些东西总是有可能失败。以下是Argonaut的透镜构造函数的几个例子:

  • def jArrayPL: Json @?> JsonArray - 仅在JSON值为数组
  • 时检索值
  • def jStringPL: Json @?> JsonString - 仅在JSON值为字符串
  • 时检索值
  • def jsonObjectPL(f: JsonField): JsonObject @?> Json - 仅在JSON对象具有字段f
  • 时检索值
  • def jsonArrayPL(n: Int): JsonArray @?> Json - 仅当JSON数组具有索引为n的元素时才检索值