13

“镜头”和“部分镜头”在名称和概念上似乎非常相似。它们有何不同?在什么情况下我需要使用其中一种?

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

4

3 回答 3

12

为了描述部分镜片——我将根据 Haskelllens命名法将其称为棱镜(但它们不是!请参阅 Ørjan 的评论)——我想先从不同的角度看待镜片本身。

镜头Lens s a表示给定一个s我们可以“关注” sat 类型的子组件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的项目组成的类型时,我们编写该类型,并且当我们有一个由要么是要么的项目组成的类型时编写。aba + b ab

在 Haskell 中,我们将元组类型写a * b(a, b)。我们写a + bEither a b, 任一类型。

产品代表捆绑在一起的数据,总和代表捆绑在一起的选项。产品可以代表有很多东西只有一个你想(一次)选择的想法,而总和代表失败的想法,因为你希望选择一个选项(比如在左侧)但相反解决另一个(沿着右边)。

最后,总和和产品是分类对偶。就像大多数 PL 所做的那样,它们合二为一,让你处于一个尴尬的境地。


因此,让我们看看当我们将上面的镜头公式(部分)二元化时会发生什么。

exists r . s ~ (r + a)

这是一个声明,要么s是类型,要么是其他东西。我们有一个类似的东西,它在其核心深处体现了选择(和失败)的概念。a rlens

这正是棱镜(或部分透镜)

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 的类型(如列表)时,它们应该感觉非常自然。

于 2015-08-26T13:44:15.663 回答
10

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

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

Haskell 库中至少有三样东西lens可以被认为是“部分镜头”,它们都与 Scala 版本不完全对应:

  • 一个普通人Lens,其“领域”是一种Maybe类型。
  • A Prism,如@J.Abrahamson 所述。
  • 一个Traversal

它们都有其用途,但前两个过于受限,无法包括所有情况,而Traversals 则“过于笼统”。这三个中,只有Traversals 支持“列表的第 n 个元素”示例。

  • 对于“Lens给一个Maybe-wrapped 值”版本,打破的是镜头法则:要拥有一个合适的镜头,您应该能够将其设置为Nothing以删除可选字段,然后将其设置回原来的样子,然后得到返回相同的值。这适用于一个Map说法(并Control.Lens.At.at为类似Map容器提供了这样的镜头),但不适用于列表,其中删除例如第0th 元素不能避免干扰后面的元素。

  • APrism在某种意义上是构造函数(在 Scala 中近似为 case 类)而不是字段的泛化。因此,它在出现时提供的“字段”应该包含所有信息以重新生成整个结构(您可以使用review函数来完成。)

  • ATraversal可以很好地执行“列表的第 n 个元素”,实际上至少有两个不同的功能ixelement并且都适用于此(但与其他容器的概括略有不同)。

由于 的 typeclass 的魔力lens, anyPrismLens自动作为 a 工作Traversal,而Lens给 a Maybe-wrapped 的可选字段可以Traversal通过与 组合成普通可选字段的 a traverse

但是,aTraversal在某种意义上过于笼统,因为它不限于单个字段:ATraversal可以有任意数量的“目标”字段。例如

elements odd

是一个Traversal将愉快地遍历列表的所有奇数索引元素,更新和/或从中提取信息的方法。

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

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

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

于 2015-08-27T00:12:12.720 回答
8

Scalaz 文档

下面是 ScalazLensFamily和的 scaladocs PLensFamily,重点是差异。

镜片:

Lens 系列,提供了一种纯粹的功能方法来访问和检索到类型的记录中从类型转换到类型的字段。是 when和。B1B2A1A2scalaz.LensA1 =:= A2B1 =:= B2

不应将术语“字段”限制性地解释为表示类的成员。例如,镜头系列可以解决Set.

部分镜头:

Partial Lens Families提供了一种纯粹的功能性方法来访问和检索到类型的记录中从类型转换类型的可选字段。是 when和。B1B2A1A2scalaz.PLensA1 =:= A2B1 =:= 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
于 2015-08-26T10:16:35.390 回答