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

13

“Lens”和“Partial Lens”在名称和概念上看起来非常相似。它们有什么区别?在什么情况下需要使用其中之一?

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


看起来 Scala 中称为 PLens 的东西在 Haskell 中被称为 prism。事实证明它是一个对 lens 的双重概念,lens 沿着积工作,而 prism 则沿着和工作。 - J. Abrahamson
3个回答

12
为了描述局部镜片——我将根据Haskell的`lens`命名法称之为棱镜(除了它们不是!请参见Ørjan的评论)——我想从不同的角度看待镜头本身。 一种方法是将`Lens s a`视为给定`s`后,我们可以“聚焦”于类型`a`的`s`的子组件,查看它,替换它,甚至(如果我们使用镜头家族变量`Lens s t a b`)改变其类型。 另一种看待这个问题的方式是`Lens s a`证明了一个同构,即一个等价关系,它使得`s`和某个未知类型`r`的元组类型`(r,a)`等价。
Lens s a ====== exists r . s ~ (r, a)

这为我们提供了所需的内容,因为我们可以将 a 取出,替换它,然后通过反向等价性重新运行以获得具有更新的 a 的新 s


现在让我们花一分钟通过代数数据类型刷新我们的高中代数。 ADT 中的两个关键操作是乘法和加法。 当我们有一个由具有 两个 ab 的项组成的类型时,我们将类型写为 a * b,而当我们有一个由 任一 ab 组成的项的类型时,我们将其写为 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

这展示了如何使用这种等价性(至少在原则上)来创建棱柱,并暗示它们应该在处理类似于列表的总和类型时感觉非常自然。

2
我认为“partial lens”和“prism”并不完全相同。请注意,Chris Martin文档粘贴列表中的“List”的“第n个元素”示例不是一个棱镜。它更像是“具有至多一个目标的遍历”,我认为在“lens”中没有它自己的类型。 - Ørjan Johansen
啊,那是个好观点 :( 我想那应该是“仿射遍历”。 - J. Abrahamson
Haskell有自己的partial lens概念,与prism不同,对吗? - Chris Martin
2
@ChrisMartin,那是一个非常不同的镜头库,我认为它没有被广泛使用。 - Ørjan Johansen

10
一个镜头是一个“功能参照”,它允许您从较大的值中提取和/或更新通用的“字段”。对于普通的、非部分的镜头,无论包含类型的任何值,该字段始终需要存在。如果您想查看像“列表的第n个元素”这样的东西,这就会产生问题,因为列表可能太短了。
因此,“部分镜头”将镜头推广到了一个字段可能或可能不总是出现在较大的值中的情况下。
在Haskell lens库中,至少有三个东西可以被视为“部分镜头”,但没有一个与Scala版本完全相对应:
  • 一个普通的Lens,其"field"是Maybe类型。
  • 一个Prism,由@J.Abrahamson描述。
  • 一个Traversal

它们都有各自的用途,但前两者太受限制,无法包含所有情况,而Traversal则“过于普遍”。在这三个中,只有Traversal支持“列表的第n个元素”这个例子。

  • 对于“给定可能包含的值的Lens”版本,它违反了镜头法则:为了拥有合适的镜头,您应该能够将其设置为Nothing以删除可选字段,然后将其设置回原来的值,然后获取相同的值。 对于像Map这样的容器 (并且Control.Lens.At.at提供了这样一个针对类似Map的容器的镜头),这个操作很好用,但对于列表则不然,例如删除第0个元素可能会干扰后面的元素。

  • 在某种意义上,Prism是构造函数(大约相当于Scala中的case class)的一种概括,而不是一个字段。因此,它存在时给出的“字段”应该包含重建整个结构所需的所有信息(您可以使用review函数完成这个操作)。

  • Traversal可以很好地处理“列表的第n个元素”,实际上至少有两个不同的函数ixelement都可以用于此操作(但是对其他容器进行了略微不同的泛化)。

由于 lens 的类型类魔法,任何 PrismLens 都会自动作为 Traversal 工作,而给定包含 Maybe 的可选字段的 Lens 可以通过与 traverse 组合将其转换为普通可选字段的 Traversal
然而,Traversal 在某种程度上过于通用,因为它不限制于单个字段:一个 Traversal 可以有任意数量的“目标”字段。例如:
elements odd

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

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

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

据我所见,技术原因是Pointed类型类不是Applicative的超类,而这个类型类需要用来定义“仿射遍历”,而普通的Traversal则需要使用Applicative

你是否偏向于 Haskell 还是 Scalaz 的设计?它们中的一个功能不足或不如另一个正确吗? - Chris Martin
@ChrisMartin 抱歉,我不太了解Scala。 - Ørjan Johansen

8

Scalaz文档

下面是Scalaz的LensFamilyPLensFamily的scaladocs,重点放在差异上。

Lens:

镜头系列,提供了一种纯函数的方式来访问和检索记录中从类型B1过渡到类型B2字段,同时记录也从类型A1过渡到类型A2。当A1 =:= A2B1 =:= B2时,scalaz.Lens是一个方便的别名。

"字段"一词不应被限制地解释为类的成员。例如,镜头系列可以处理集合Set的成员

Partial lens:

部分镜头系列,提供了一种纯函数的方式来访问和检索记录中从类型B1过渡到类型B2可选字段,同时记录也从类型A1过渡到类型A2scalaz.PLens是一个方便的别名,当A1 =:= A2B1 =:= B2时。

"字段"一词不应被限制地解释为类的成员。例如,部分镜头系列可以处理列表List的第n个元素

符号表示法

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

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

在中缀表示法中,从类型为A的记录中检索类型为B的字段的镜头类型表示为A @> B,部分镜头表示为A @?> B

Argonaut

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的元素时检索值

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接