“Lens”和“Partial Lens”在名称和概念上看起来非常相似。它们有什么区别?在什么情况下需要使用其中之一?
标记Scala和Haskell,但欢迎与任何拥有镜头库的函数式语言相关的解释。
Lens s a ====== exists r . s ~ (r, a)
a
取出,替换它,然后通过反向等价性重新运行以获得具有更新的 a
的新 s
。
现在让我们花一分钟通过代数数据类型刷新我们的高中代数。 ADT 中的两个关键操作是乘法和加法。 当我们有一个由具有 两个 a
和 b
的项组成的类型时,我们将类型写为 a * b
,而当我们有一个由 任一 a
或 b
组成的项的类型时,我们将其写为 a + b
。
在 Haskell 中,我们将 a * b
写为元组类型 (a, b)
。我们将 a + b
写为 either 类型 Either a b
。
产品表示捆绑数据在一起,总和表示捆绑 选项 在一起。产品可以表示拥有许多东西,只有其中一个您想要选择(每次)的想法,而总和表示失败的想法,因为您希望选择一个选项(在 左侧 上,例如),但实际上不得不接受另一个选项(沿着 右侧)。
最后,总和和乘积是范畴对偶。它们 配合,没有其中之一,正如大多数 PL 所做的那样,会让您处于尴尬的境地。
因此,让我们看一下当我们对上述镜头公式的一部分进行对偶时会发生什么。
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
,以使它们互相反转。让我们编写这个函数来描述我们的棱镜。
prism :: (s ~ exists r . Either r a) -> Prism s a
uncons = prism (iso fwd bck) where
fwd [] = Left () -- failure!
fwd (a:as) = Right (a, as)
bck (Left ()) = []
bck (Right (a, as)) = a:as
lens
库中,至少有三个东西可以被视为“部分镜头”,但没有一个与Scala版本完全相对应:
它们都有各自的用途,但前两者太受限制,无法包含所有情况,而Traversal
则“过于普遍”。在这三个中,只有Traversal
支持“列表的第n个元素”这个例子。
对于“给定可能包含的值的Lens
”版本,它违反了镜头法则:为了拥有合适的镜头,您应该能够将其设置为Nothing
以删除可选字段,然后将其设置回原来的值,然后获取相同的值。 对于像Map
这样的容器 (并且Control.Lens.At.at
提供了这样一个针对类似Map
的容器的镜头),这个操作很好用,但对于列表则不然,例如删除第0
个元素可能会干扰后面的元素。
在某种意义上,Prism
是构造函数(大约相当于Scala中的case class)的一种概括,而不是一个字段。因此,它存在时给出的“字段”应该包含重建整个结构所需的所有信息(您可以使用review
函数完成这个操作)。
Traversal
可以很好地处理“列表的第n个元素”,实际上至少有两个不同的函数ix
和element
都可以用于此操作(但是对其他容器进行了略微不同的泛化)。
lens
的类型类魔法,任何 Prism
或 Lens
都会自动作为 Traversal
工作,而给定包含 Maybe
的可选字段的 Lens
可以通过与 traverse
组合将其转换为普通可选字段的 Traversal
。Traversal
在某种程度上过于通用,因为它不限制于单个字段:一个 Traversal
可以有任意数量的“目标”字段。例如:elements odd
这是一个Traversal
,它可以愉快地遍历列表中所有奇数索引的元素,并从中更新和/或提取信息。
理论上,你可以定义第四个变量(@J.Abrahamson提到的“仿射遍历”),我认为这可能更接近Scala版本,但由于技术原因不在库本身之外,它们与库的其余部分不匹配 - 你必须明确地转换这样的“部分镜头”,以使用其中一些Traversal
操作。
此外,与普通的Traversal
相比,它并没有带来太多好处,因为例如有一个简单的运算符(^?)
可以提取遍历的第一个元素。
Pointed
类型类不是Applicative
的超类,而这个类型类需要用来定义“仿射遍历”,而普通的Traversal
则需要使用Applicative
。下面是Scalaz的LensFamily
和PLensFamily
的scaladocs,重点放在差异上。
Lens:
镜头系列,提供了一种纯函数的方式来访问和检索记录中从类型
B1
过渡到类型B2
的字段,同时记录也从类型A1
过渡到类型A2
。当A1 =:= A2
,B1 =:= B2
时,scalaz.Lens
是一个方便的别名。"字段"一词不应被限制地解释为类的成员。例如,镜头系列可以处理集合
Set
的成员。
Partial lens:
部分镜头系列,提供了一种纯函数的方式来访问和检索记录中从类型
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]
A
的记录中检索类型为B
的字段的镜头类型表示为A @> B
,部分镜头表示为A @?> B
。
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
的元素时检索值