为了理解这一点,您必须了解类型、类型变量、代数数据类型(和和乘积类型),并且还必须了解类型类和Functor
特别是类型类。如果您不理解这些内容,请为此页面添加书签,然后继续理解它们,可能会使用我为此目的而帮助开发的资源,以说明和解释这些基础知识:http://happylearnhaskelltutorial.com http://happylearnhaskelltutorial.com
因此,在讨论棱镜之前,您首先需要知道什么是镜头。
A Lens
通常被描述为函数式 getter/setter,但这更多的是关于实现等等。
我现在想和你一起尝试一下描述的实验。
假设我有一页非常小的文字,上面有一些文字。现在,我递给你一块与该页面大小相同的纸板,只是它有一个用于放大镜的孔,用于聚焦于特定单词。该词位于该页面的特定位置。
所以我们有这两件事:一个页面和一个没有放大镜的放大镜“卡片”......如果我们放入一个玻璃,它会聚焦在页面上的特定位置。现在有人带来了另一页,上面有不同的文字,但布局与第一页相同。
很容易想象,您可以拿起那张卡片,将其放在新页面上,它会集中在同一位置的不同单词上。
除了放大镜之外,您还有一个特殊的“橡皮擦/打印机”玻璃,当您将其放入卡片中时,您可以擦除并在该页面上键入文字。
现在将其应用到镜头上,您可以通过这个示例看到,如果我们获取一条数据(一页文本),一个适合该数据的镜头(一张带有孔的卡片,与页面的形状相匹配) )和一个可以“获取”或“设置”(或者可能是其他东西)的函数(即放大镜或橡皮擦/打印机玻璃,或其他类型的眼镜),好吧,由此我们可以view
从较大的数据(页面)中提取较小的数据(单词)来提取它......或者也许我们可以set
将不同的匹配数据插入该页面上的位置...
lens :: :: Functor f => (s -> a) -> (s -> b -> t) -> (a -> f b) -> s -> f t
这个函数有什么作用?它creates具有某些功能的镜头。现在我们有了上面的框架了解了什么是镜头,我们就可以了解这个功能了。我对你说s
变量代表“状态”,它对应于镜头聚焦的纸张类型。接下来我可以说的是a
type 变量对应于镜头将聚焦的页面上的单词。关于什么b
and t
?他们是改造后的a
and s
如果我们决定改变值a
,并且这种变化改变了它的类型。
是什么Functor
那么呢?我们一会儿就会知道。好吧,首先让我们制作一个镜头来实现这一点。那么,回到我们的lens
函数,它的第一个参数是“getter”函数,第二个参数是“setter”函数(因此是类型)。然后还有另一种说法。嗯,因为 Haskell 函数是柯里化的,所以实际上是返回类型:一个函数s -> f t
。现在让我们制作那个镜头。
假设我们有一个值列表[(1,(5,9)), (2,(3,6))]
我们想要制作一个聚焦于第二个嵌套元组中的第二个值的镜头。但这很愚蠢,因为你可以使用snd . snd
正确的?是的,你可以,所以这不是一个很好的例子,但是所有更好的例子都更复杂,所以请耐心等待 - 我们会讲到它们,此外,你能snd.snd
函数也设置,或者有一个函数应用于它?不,我不这么认为! :)(好吧好吧我知道你可以用fmap (const val)
设置,但它也可以改变类型吗?好吧,如果你像 Edward Kmett 那样继续得出逻辑结论,那么实际上你最终会得到 Lens 的这种思路 - 这就是 Functor 的用武之地!)
sndOfSndLens = lens (snd.snd) (\(x,(y,z)) newZ -> (x,(y,newZ)))
那么我们得到了什么?我们得到了这种类型的函数sndOfSndLens :: Functor f => (a -> f t2) -> (t, (t1, a)) -> f (t, (t1, t2))
.
因此,让我们明确我们的价值观:map (view sndOfSnd) [(1,(5,9)), (2,(3,6))]
-> [9,6]
好的!这有效...让我们在以下位置设置新值:map (set sndOfSnd 2000) [(1,(5,9)), (2,(3,6))]
-> [(1,(5,2000)),(2,(3,2000))]
好的...
如果只是 getter 或 setter,那很无聊,但还有一个函数叫做over
它将采用一个镜头和一个变换函数,并在镜头焦点函数上运行该变换......所以让我们从每个函数中减去 10:map (over sndOfSnd (flip (-) 10)) [(1,(5,9)), (2,(3,6))]
-> [(1,(5,-1)),(2,(3,-4))]
这很酷!好吧,我会让您阅读镜头文档的其余部分,以了解所有其他功能,因为它们很深并且镜头组成,您也可以用它们做各种其他事情。
Prism
我答应我们会到达棱镜,看看我们在这里......镜头是一个特定的“光学器件”(同时,有时也令人困惑地指代整套光学器件,所以要小心) ,棱镜是另一种光学器件,幸运的是完全具体。如果镜头是一种用于聚焦于物体特定部分的光学器件product 代数数据类型,然后 Prims 做同样的事情sum类型。这就是为什么我们能够制作一款谈论配对的镜头((,)
)因为他们是product类型...也就是说,它们将两种类型合二为一。镜头可让您专注于一件作品或穿过这些作品的路径。顺便说一句,我们上面创建的镜头可以通过组合更通用的内置镜头来轻松定义:_2 . _2
。还有我们讨论的所有镜头功能的操作员版本。他们看起来很疯狂,但他们有逻辑。了解有关他们的信息!
棱镜让您专注于一个path通过一个sum type。有什么好的例子吗?好吧,假设我们已经考虑过Either
数据类型。它是Either a b
它被定义为data Either a b = Left a | Right b
。于是就有了对应的prism
函数让我们构建一个与上面的值相同的 Prism。如果我们使用内置的会发生什么_Left
它集中在左侧Either
,但我们只有一个Right 10
价值?让我们看看...但首先,我们应该让您知道我们不能使用view
不再需要,因为它可能不起作用,所以我们需要使用preview
这将返回一个可能的值fail(抱歉,剧透!):
preview _Left (Left 10)
-> Just 10
然后是Right
one? preview _Left (Right 10)
-> Nothing
。好吧,这很甜蜜。
The set
函数工作正常,因为如果它没有意义,它可能会默默地失败:set _Left 30 (Left 10)
-> Left 30
。当它不起作用时会发生什么?set _Right 30 (Left 10)
-> Left 10
没错,什么都没有。
酷...希望这能解释镜头和棱镜。它们是两个非常有用的光学器件。镜头库是full所以我鼓励你看看它们
那原来的问题呢?
原来的问题是关于makeLenses
and makePrisms
。这些是模板 haskell 表达式(也就是说,它们是元编程/类似于宏,但类型宏),它们允许您根据自己的数据类型自动构建自己的镜头和/或棱镜。希望现在当您选择其中之一以及它们有何不同时,它会更有意义。这至少会让您大致了解它们的不同之处。要真正理解,您应该阅读文档并查找所有其他可能的功能和光学器件。
欢迎来到超酷的镜头世界!