Haddocks是最深入的资源,包含所有内容,但刚开始使用可能有点难以导航。只需浏览不同模块,做好心理笔记,很快就能找到方向。你链接的图表也是各个模块的非常好的地图。
然而,既然你说你不理解图形,那我就假设你不需要高级或完整的参考。该图实际上是lens
软件包部件的一个非常基本的高层概述。如果你不理解图表,最好还是先等一段时间再进行高级操作。
阅读此内容时,请记住,虽然lens
软件包最初是“镜头”软件包,但现在软件包中有许多种不同类型的光学工具,它们遵循不同的法则并用于不同的用途。“光学工具”是您用于探测数据结构的类似镜头的通用名称。
无论如何,以下是我如何阅读该图的方法。
现在,只看写有光学器名称的框的顶部部分。
顶层,即Fold
和Setter
,是lens
的基本光学器。正如你可以想象的那样,Setter
是一种设置某个字段值的光学器。这里省略了一堆示例,因为你说你已经掌握了大部分基础知识,但本质上当你执行
λ> ellen & age .~ 35
Person { _name = "Ellen", _age = 35 }
如果你曾经用过 age
作为一个 Setter
,那么你已经了解了它。
Getter
是一种特殊的 Fold
,允许你从数据结构中获取值。(Fold
只允许你将值以某种方式组合成新值,而不是将它们取出。) 在图表中,从 Getter
指向 Fold
的箭头标注了 Getter
是一种特殊的 Fold
。
如果你将一个 Fold
和一个 Setter
(即将循环遍历一堆值和设置单个值的方法组合在一起),你就会得到一个 Traversal
。 Traversal
是一个瞄准多个元素的光学器,并允许你设置或修改它们。
进一步地,如果你将一个 Getter
与一个 Traversal
结合起来,你就会得到一个 Lens
。这对你来说应该很熟悉,因为“getter 和 setter 的组合”通常被称为镜头,而你可以将 Traversal
视为更强大的 Setter
。
我不会假装我对 Review
、Prism
、Iso
和 Equality
之间的关系非常了解。据我所知,
Review
是一个围绕着函数 b -> t
的基本包装器,其中 b
应该是 t
结构中的一个字段。因此,Review
接受单个字段值,然后构造一个整个结构。这可能看起来很愚蠢,但它实际上是 Getter
的相反,用于构建棱镜。Prism
允许您在分支类型中获取和设置值。它对于 Either
来说就像 Lens
对于元组一样。您无法仅使用镜头获取 Either
中的值,因为如果它遇到 Left
分支,它将会崩溃。Iso
是 Lens
和 Prism
的非常强大的组合,允许您自由地通过光学器 "正反两面" 地查看数据。虽然 Lens
让您从高层次上看到数据结构的精确部分,但 Iso
还允许您从数据结构的精确部分向高层次查看。Equality
是什么,抱歉。中断: 类型签名
类型签名如 Lens s t a b
一开始可能会让人感到害怕,所以我将尽快解释其含义。如果你查看 Getter
,你会发现它的类型签名是:
Getter s a
如果我们思考一下getter的概念,这可能会很熟悉。什么是getter?它是一个从数据结构s到该结构内部单个值a
的函数。例如,Person -> Age
函数是从Person
对象中获取年龄的getter。相应的Getter
只需具有以下签名:
Getter Person Age
就是这么简单。一个 Getter s a
是一种能够从 s
内部获取 a
的光学元件。
现在,经过简化的 Lens'
签名应该不那么可怕了。如果你必须猜测,a 是什么?
Lens' Person Age
很简单!它是一个Lens
,可以获取和设置(记得镜头是获取器和设置器的组合吗?)Person
值内的 Age
字段。因此,您应用于Getter s a
的相同逻辑也可以应用于Lens' s a
。
那么s t a b
部分呢?好吧,考虑一下这个类型:
data Person a = { _idField :: a, address :: String }
一个人可以通过某个身份标识值进行识别,并拥有年龄。假设你是通过名字进行识别的。
carolyn :: Person String
carolyn = Person "Carolyn" "North Street 12"
如果你有一个函数toIdNumber :: String -> Int
,可以将字符串转换为ID号码,你可能希望这样做:
λ> carolyn & idField %~ toIdNumber
Person 57123 "North Street 12"
但是如果你使用 idField :: Lens' (Person String) String
,你不能将字符串转换为整数并放入相同的位置,因为该镜头只能处理 String
。这就是为什么我们需要 Lens s t a b
。
我理解这个签名的方式是“如果你给我一个函数 a -> b
,我会给你一个函数 s -> t
”,其中 a
和 b
都指代镜头的目标,而 s
和 t
指代包含数据结构。具体例子:如果我们有:
idField :: Lens (Person String) (Person Int) String Int
我们可以进行上述的转换。该镜头知道如何将具有字符串id字段的Person
转换为具有整数id字段的person。Lens s t a b
可以被解读为一个“函数”(a -> b) -> (s -> t)
。“给我一个在目标上执行的转换(a -> b)
以及包含该目标的数据结构s
,我将返回您一个应用了该转换的数据结构t
”。Setter
中的over
,您可以将其变成该函数。
Getter
框中,顶部项目是to
函数,它从任何函数s -> a
构建Getter
。如果使用traverse
函数,则可以从Traversable
中免费获得Traversal
。Getter
下找到view
,这是您如何使用Getter
从数据结构中获取某些内容。在Setter
中,您会发现over
和set
很高。to :: (s -> a) -> Getter s a
view :: Getter s a -> (s -> a)
unto :: (b -> t) -> Review s t a b
review :: Review s t a b -> (b -> t)
我通常不会像写这篇文章一样那样仔细地研究图形。大多数情况下,我只是偷看Setter
、Getter
、Traversal
、Lens
和Prism
以及它们相互之间的关系,因为这些光学器件是我最常使用的。
当我知道我需要做某件事情时,通常只需快速查看图形,以查看哪些框包含与我想要执行的操作类似的操作。(例如,如果我有一个data Gendered a = Masculine a | Feminine a
,那么_Right
与假设的_Feminine
类似。)在确定后,我会深入研究这些模块的Haddocks(在这个例子中,是Prism
),寻找我需要的操作。
我学习光学器件和如何使用它们的方式与我学习所有东西的方式相同。我找到一个光学器件是一个好的解决方案的实际问题,并尝试解决它。我尝试并失败,然后再次尝试并寻求帮助,我不断尝试、不断尝试。最终我会成功。然后我尝试一些稍微不同的东西。
通过实际使用它们的方式,我收集了很多有用的使用经验,知道它们如何工作等等。
在我开始写这篇文章之前,我认为你需要Prism
来处理Maybe
值,因为Maybe
类型是分支的,不是吗?不完全是!至少不比List
类型更分支,而你不需要Prism
来处理List
类型。
因为Maybe
类型是一个零或一个元素的容器,所以你实际上只需要一个Traversal
来处理它。当你有两个可能包含不同值的支路时,你开始需要Prism
的全部功力。(要构造一个Maybe
值仍然需要Review
)
我仅当我开始仔细阅读图表并探索模块以找出光学器件之间的实际、正式差异时才明白了这一点。这是使用我的学习方法的缺点。只要它发挥作用,我就凭直觉做事。有时这会导致绕远路。