镜片和局部镜片有什么区别?

What's the difference between a lens and a partial lens?

A "lens" 和 A "partial lens" 在名称和概念上似乎很相似。它们有何不同?在什么情况下我需要使用其中之一?

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

Scalaz 文档

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

镜头:

A Lens Family, offering a purely functional means to access and retrieve a field transitioning from type B1 to type B2 in a record simultaneously transitioning from type A1 to type A2. scalaz.Lens is a convenient alias for when A1 =:= A2, and B1 =:= B2.

The term "field" should not be interpreted restrictively to mean a member of a class. For example, a lens family can address membership of a Set.

部分镜头:

Partial Lens Families, offering a purely functional means to access and retrieve an optional field transitioning from type B1 to type B2 in a record that is simultaneously transitioning from type A1 to type A2. scalaz.PLens is a convenient alias for when A1 =:= A2, and B1 =:= B2.

The term "field" should not be interpreted restrictively to mean a member of a class. For example, a partial lens family can address the nth element of a List.

符号

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

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

在中缀表示法中,这意味着从A类型的记录中检索B类型的字段的镜头类型表示为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
  • 处有一个元素时才检索值

为了描述部分透镜——我以后将根据 Haskell lens 命名法称之为棱镜(除了它们不是!请参阅 Ørjan 的评论)——我会喜欢从不同的角度看待镜头本身。

镜头 Lens s a 表示给定一个 s 我们可以 "focus" 在类型 as 的子组件上,查看它,替换它, 并且(如果我们使用镜头系列变体 Lens s t a b)甚至改变它的类型。

一种看待这个问题的方法是 Lens s a 见证了 s 和元组类型 (r, a) 之间的同构,等价性 unknown 输入 r.

Lens s a ====== exists r . s ~ (r, a)

这给了我们所需要的,因为我们可以把 a 拉出来,替换它,然后 运行 东西通过向后的等效得到一个新的 s已更新 a.


现在让我们花点时间通过代数数据类型来复习一下我们的高中代数知识。 ADT 中的两个关键操作是乘法和求和。当我们有一个类型由具有 both 一个 a 和一个 b 的项目组成时,我们写类型 a * b 并且我们写 a + b 当我们有一个由 ab.

项组成的类型时

在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

那么对于一些简单的例子,这是如何工作的?

好吧,考虑"unconses"列表中的棱镜:

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

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

镜头是一个"functional reference",允许您提取and/or更新一个更大的值的广义"field"。对于普通的非部分镜头,对于包含类型的任何值,该字段始终需要 there。如果您想查看可能并不总是存在的 "field" 之类的内容,则会出现问题。例如,在 "the nth element of a list" 的情况下(如 Scalaz 文档中所列@ChrisMartin 粘贴),列表可能太短。

因此,"partial lens" 将镜头概括为场可能会或可能不会始终以较大值存在的情况。

Haskell lens 库中至少有三个东西可以认为是 "partial lenses",其中 none 与 Scala 版本完全对应:

  • 普通 Lens 其 "field" 是 Maybe 类型。
  • A Prism,如@J.Abrahamson所述。
  • 一个Traversal.

它们都有自己的用处,但前两个限制太多,无法包含所有情况,而 Traversal 是 "too general"。在这三个中,只有 Traversal 支持 "nth element of list" 示例。

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

  • A Prism 在某种意义上是 构造函数 的泛化(在 Scala 中大约是 class 的情况)而不是字段.因此,它在存在时给出的 "field" 应该包含 all 重新生成整个结构的信息(您可以使用 review 函数。)

  • A Traversal 可以做 "nth element of a list" 就好了,事实上至少有两个不同的函数 ix and element 都适用于此(但概括略有不同到其他容器)。

感谢 lens 的类型class 魔法,任何 PrismLens 自动作为 Traversal,而 Lens 通过与 traverse.

组合,可以将 Maybe 包装的可选字段转换为普通可选字段的 Traversal

然而,Traversal 在某种意义上 太笼统了 ,因为它不限于单个字段:Traversal 可以有 任意 个"target" 字段。例如

elements odd

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

理论上,您可以定义第四个变体("affine traversals" @J.Abrahamson 提到的),我认为它可能更接近于 Scala 的版本,但由于 lens 库本身它们与库的其余部分不太适合 - 您必须显式转换这样的 "partial lens" 才能使用它的某些 Traversal 操作。

此外,与普通 Traversal 相比,它不会给您带来太多好处,因为例如一个简单的运算符 (^?) 仅提取遍历的第一个元素。

(据我所知,技术原因是定义 "affine traversal" 所需的 Pointed 类型 class 不是超级 class of Applicative,普通 Traversal 使用。)