“镜头”和“部分镜头”在名称和概念上似乎非常相似。它们有何不同?在什么情况下我需要使用其中一种?
标记 Scala 和 Haskell,但我欢迎与任何具有镜头库的函数式语言相关的解释。
为了描述部分镜片——我将根据 Haskelllens
命名法将其称为棱镜(但它们不是!请参阅 Ørjan 的评论)——我想先从不同的角度看待镜片本身。
镜头Lens s a
表示给定一个s
我们可以“关注” s
at 类型的子组件a
,查看它,替换它,并且(如果我们使用镜头系列变体Lens s t a b
)甚至更改它的类型。
看待这一点的一种方法是见证一些未知类型的元组类型Lens s a
之间的同构、等价。s
(r, a)
r
Lens s a ====== exists r . s ~ (r, a)
这给了我们我们需要的东西,因为我们可以拔出a
,替换它,然后通过等价向后运行,以获得一个s
没有更新的新的a
。
现在让我们花一点时间通过代数数据类型来刷新我们的高中代数。ADT 中的两个关键运算是乘法和求和。当我们有一个由同时具有an和 aa * b
的项目组成的类型时,我们编写该类型,并且当我们有一个由要么是要么的项目组成的类型时编写。a
b
a + b
a
b
在 Haskell 中,我们将元组类型写a * b
为(a, b)
。我们写a + b
为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
,我们需要编写一种方法来将 ana
转换为 ab
并将 ab
转换为 an 以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
这演示了如何使用这种等价性(至少在原则上)来创建棱镜,并且还表明当我们使用类似 sum 的类型(如列表)时,它们应该感觉非常自然。
镜头是一种“功能参考”,可让您提取和/或更新更大值的广义“场”。对于普通的非部分镜头,对于包含类型的任何值,该字段始终需要存在。如果您想查看可能并不总是存在的“字段”之类的东西,这就会出现问题。例如,在“列表的第 n 个元素”的情况下(如 Scalaz 文档@ChrisMartin 粘贴的那样),列表可能太短。
因此,“部分透镜”将透镜概括为场可能或可能不总是以较大值存在的情况。
Haskell 库中至少有三样东西lens
可以被认为是“部分镜头”,它们都与 Scala 版本不完全对应:
它们都有其用途,但前两个过于受限,无法包括所有情况,而Traversal
s 则“过于笼统”。这三个中,只有Traversal
s 支持“列表的第 n 个元素”示例。
对于“Lens
给一个Maybe
-wrapped 值”版本,打破的是镜头法则:要拥有一个合适的镜头,您应该能够将其设置为Nothing
以删除可选字段,然后将其设置回原来的样子,然后得到返回相同的值。这适用于一个Map
说法(并Control.Lens.At.at
为类似Map
容器提供了这样的镜头),但不适用于列表,其中删除例如第0
th 元素不能避免干扰后面的元素。
APrism
在某种意义上是构造函数(在 Scala 中近似为 case 类)而不是字段的泛化。因此,它在出现时提供的“字段”应该包含所有信息以重新生成整个结构(您可以使用review
函数来完成。)
ATraversal
可以很好地执行“列表的第 n 个元素”,实际上至少有两个不同的功能ix
,element
并且都适用于此(但与其他容器的概括略有不同)。
由于 的 typeclass 的魔力lens
, anyPrism
或Lens
自动作为 a 工作Traversal
,而Lens
给 a Maybe
-wrapped 的可选字段可以Traversal
通过与 组合成普通可选字段的 a traverse
。
但是,aTraversal
在某种意义上过于笼统,因为它不限于单个字段:ATraversal
可以有任意数量的“目标”字段。例如
elements odd
是一个Traversal
将愉快地遍历列表的所有奇数索引元素,更新和/或从中提取信息的方法。
从理论上讲,您可以定义第四个变体(@J.Abrahamson 提到的“仿射遍历”),我认为它可能更接近于 Scala 的版本,但由于lens
库本身之外的技术原因,它们不能很好地适应其余的库的 - 您必须明确转换这样的“部分镜头”才能使用Traversal
它的一些操作。
此外,它不会比普通Traversal
的 s 买多少,因为例如有一个简单的运算符(^?)
来提取遍历的第一个元素。
(据我所知,技术原因是Pointed
定义“仿射遍历”所需的类型类不是Applicative
普通Traversal
s 使用的超类。)
下面是 ScalazLensFamily
和的 scaladocs PLensFamily
,重点是差异。
镜片:
Lens 系列,提供了一种纯粹的功能方法来访问和检索到类型的记录中从类型转换到类型的字段。是 when和。
B1
B2
A1
A2
scalaz.Lens
A1 =:= A2
B1 =:= B2
不应将术语“字段”限制性地解释为表示类的成员。例如,镜头系列可以解决
Set
.
部分镜头:
Partial Lens Families提供了一种纯粹的功能性方法来访问和检索到类型的记录中从类型转换类型的可选字段。是 when和。
B1
B2
A1
A2
scalaz.PLens
A1 =:= A2
B1 =:= B2
不应将术语“字段”限制性地解释为表示类的成员。例如,部分镜头系列可以解决 a 的第 n 个元素
List
。
对于不熟悉 scalaz 的人,我们应该指出符号类型别名:
type @>[A, B] = Lens[A, B]
type @?>[A, B] = PLens[A, B]
在中缀表示法中,这意味着从类型B
记录中检索类型字段的镜头类型A
表示为A @> B
,部分镜头表示为A @?> B
。
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