“镜头”和“部分镜头”在名称和概念上看起来相似。他们有什么不同?在什么情况下我需要使用其中一种?
标记Scala和Haskell,但我欢迎与任何具有镜头库的功能语言相关的解释。
答案 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中的两个关键操作是乘法和求和。当我们的类型包含 a
和a * b
的项目时,我们会写出a
类型,当我们编写b
时的类型包含 a + b
或a
在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版本:
它们都有它们的用途,但前两个太受限制而不包括所有情况,而Traversal
s“太笼统”。在这三个中,只有Traversal
支持“列表的第n个元素”示例。
对于“Lens
给出Maybe
- 包裹值”版本,镜片定律会有所不同:要有合适的镜头,您应该能够将其设置为{{ 1}}删除可选字段,然后将其设置回原来的状态,然后返回相同的值。这适用于Nothing
说(并且Control.Lens.At.at
为Map
- 类似容器提供了这样的镜头),但不适用于列表,其中删除例如Map
元素不能避免扰乱后者。
从某种意义上说,0
是构造函数(大约是Scala中的case类)而不是字段的泛化。因此,它所提供的“字段”应该包含所有信息以重新生成整个结构(您可以使用Prism
函数。)
review
可以做“列表的第n个元素”就好了,事实上至少有两个不同的函数ix
和element
都适用于此(但与其他容器略有不同。)
感谢Traversal
的类型类魔术,任何lens
或Prism
自动作为Lens
,而Traversal
给出Lens
通过与Maybe
合成,可以将已包装的可选字段转换为普通可选字段的Traversal
。
但是,traverse
在某种意义上太一般,因为它不限于单个字段:Traversal
可以任何“目标”字段的数量。 E.g。
Traversal
是一个elements odd
,它会愉快地浏览列表中所有奇数索引的元素,更新和/或从中提取所有信息。
理论上,您可以定义第四个变体(“仿射遍历”@ J.Abrahamson提及)我认为可能更接近于Scala的版本,但由于Traversal
库本身之外的技术原因它们不适合图书馆的其他部分 - 您必须明确转换这样的“部分镜头”以使用它的一些lens
操作。
此外,它不会比普通的Traversal
更多地购买你,因为例如一个简单的运算符(^?)
,只提取遍历的第一个元素。
(据我所知,技术原因是定义“仿射遍历”所需的Pointed
类型类不是Traversal
的超类,普通{{1}使用。)
答案 2 :(得分:8)
以下是Scalaz LensFamily
和PLensFamily
的scaladocs,并强调了差异。
镜头:
A Lens Family,提供了一种纯粹的功能性方法,可以访问和检索 一个字段 ,从类型
B1
转换为B2
类型记录同时从类型A1
转换为类型A2
。scalaz.Lens
是A1 =:= A2
和B1 =:= B2
的便捷别名。术语"字段"不应该被限制性地解释为一个类的成员。例如, 镜头系列可以解决
Set
的成员资格。
部分镜头:
Partial Lens Families,提供了一种纯粹功能性的方法来访问和检索 可选字段 从类型
B1
转换为类型B2
在同时从类型A1
转换为类型A2
的记录中。scalaz.PLens
是A1 =:= A2
和B1 =:= 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
的元素时才检索值