我在看Control.Lens
介绍video
这让我想知道为什么Setter
类型需要在函子中包装东西
它(大致)定义如下:
type Control.Lens.Setter s t a b = (Functor f) => (a -> f a) -> s -> f t
假设我有一个名为Point
的数据,其定义如下:
data Point = Point { _x :: Int, _y :: Int } deriving Show
然后我可以这样写自己的xlens
:
type MySetter s t a b = (a -> b) -> s -> t
xlens :: MySetter Point Point Int Int
xlens f p = p { _x = f (_x p) }
我可以这样使用它:
p = Point 100 200
xlens (+1) p -- Results in Point { _x = 101, _y = 200 }
通过使用Control.Lens
,可以通过以下方式实现相同的效果:
over x (+1) p
以下内容:
x :: Functor f => (Int -> f Int) -> Point -> f Point
over :: Setter Point Point Int Int -> (Int -> Int) -> Point -> Point
所以我的问题是,既然可以用更简单的方式实现相同的效果,为什么Control.Lens
将事物包装在仿函数中呢?这看起来很多,因为我的xlens
与Control.Lens
的{{1}}相同。
只是为了记录,我也可以用同样的方式链接我的镜头:
over x
答案 0 :(得分:17)
这是一个很好的问题,需要一点点拆包。
我想在蝙蝠的一点上轻轻地纠正你:lens
包中的Setter的类型是最新版本的
type Setter s t a b = (a -> Identity b) -> s -> Identity t
尚未看到Functor
。
然而,这不会使您的问题无效。为什么不是这种类型
type Setter s t a b = (a -> b) -> s -> t
为此,我们首先要讨论Lens
。
Lens
是一种允许我们同时执行getter和setter操作的类型。这两者结合形成了一个美丽的功能参考。
Lens
类型的简单选择是:
type Getter s a = s -> a
type Setter s t a b = (a -> b) -> s -> t
type Lens s t a b = (Getter s a, Setter s t a b)
然而,这种类型非常不满意。
.
一起构成,这可能是lens
包的唯一最佳卖点。view
)和定位器(如over
)不能拍摄镜头,因为它们的类型非常不同。没有最后一个问题解决了为什么甚至打扰写一个库?我们讨厌用户不得不经常考虑它们在UML光学层次结构中的位置,每次上下移动时调整它们的函数调用。
现在的问题是:我们可以为Lens
写下一种类型,以便它自动 Getter
和{{1} }?为此,我们必须转换Setter
和Getter
的类型。
首先请注意,Setter
相当于s -> a
。这种转变为延续传递方式远非显而易见。你或许能够直观地理解这个转变:“forall r. (a -> r) -> s -> r
类型的函数是一个承诺,任何s -> a
你可以给我一个s
。但这应该等同于承诺给定一个将a
映射到a
的函数,您可以向我提供一个将r
映射到s
的函数。“也许?也许不吧。这里可能会有一个信仰的飞跃。
现在定义r
。请注意,newtype Const r a = Const r deriving Functor
在数学上和运行时与Const r a
相同。
现在请注意,r
可以重写为type Getter s a = forall r. (a -> r) -> s -> r
。虽然我们为自己引入了新类型变量和精神痛苦,但这种类型在数学上仍然与我们开始时的相同(type Getter s t a b = forall r. (a -> Const r b) -> s -> Const r t
)。
定义s -> a
。请注意,newtype Identity a = Identity a
在数学上和运行时与Identity a
相同。
现在请注意,a
仍然与我们开始使用的类型相同。
通过这个文书工作,我们可以将setter和getter统一为一个type Setter s t a b = (a -> Identity b) -> s -> Identity t
类型吗?
Lens
这是Haskell,我们可以将type Setter s t a b = (a -> Identity b) -> s -> Identity t
type Getter s t a b = forall r. (a -> Const r b) -> s -> Const r t
或Identity
的选择抽象为量化变量。作为the lens wiki says,Const
和Const
的共同点是每个都是Identity
。然后我们选择它作为这些类型的统一点:
Functor
(还有其他理由选择type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
,例如通过使用自由定理证明函数引用的定律。但是我们会在这里稍微移动一下。)Functor
是比如forall f
。上面 - 它允许该类型的消费者选择如何填充变量。填写forall r
并获得一个setter。填写Identity
即可获得吸气剂。通过选择我们能够在此时到达的方式进行小而细致的转换。
值得注意的是,此推导不 Const a
包的原始动机。正如Derivation wiki page states所解释的那样,您可以从具有某些功能的lens
的有趣行为开始,并从那里取出光学元件。但是我认为我们制定的这条道路在解释你提出的问题方面要好一些,这也是我开始提出的一个大问题。我还想推荐你lens over tea,它提供另一个派生。
我认为这些多重推导是一件好事,也是(.)
设计健康性的一种量油尺。我们能够从不同角度得出同样优雅的解决方案,这意味着这种抽象是强大的,并得到不同直觉和数学的良好支持。
我还对最近lens
中的Setter类型撒了一些谎言。它实际上是
lens
这是抽象光学类型中的高阶类型的另一个例子,为图书馆用户提供更好的体验。几乎总是将type Setter s t a b = forall f. Settable f => (a -> f b) -> s -> t b
实例化为f
,因为有Identity
。但是,您可能偶尔会将setter传递给instance Settable Identity
函数,该函数会将backwards
修复为f
。我们可以将此段落归类为“有关Backwards Identity
的更多信息,而不是您可能想知道的。”
答案 1 :(得分:6)
从某种意义上说,lens
在函子返回中包装setter的原因是它们太强大了。
事实上,当使用 时,无论如何,仿函数将被实例化为Identity
,这与您提出的签名完全相同。但是, setter的实现不得利用此事实。有了你的签名,我可以写出像
zlens :: MySetter Point Point Int Int
zlens _f p = p -- no z here!
嗯,基于Functor
的签名是不可能的,因为zlens
需要通过仿函数进行普遍量化,它无法知道如何将结果注入{{1}包装器。获得仿函数类型结果的唯一方法是首先将setter函数应用于正确类型的字段!
所以,这只是一个很好的自由定理†。
更实际的是,我们需要兼容性的仿函数包装器。虽然您可以在没有此包装器的情况下定义 setters ,但这对于getter来说是不可能的,因为它们使用f
而不是Const
,并且需要在此第一个参数中添加多态性类型构造函数。通过要求所有镜头风格的包装(仅具有不同的类限制),我们可以为所有这些使用相同的组合器,但类型系统将始终将功能折叠到实际的任何功能适用于这种情况。
† 考虑到这一点,保证实际上并不是很强大......我仍然可以用一些Identity
骗子来颠覆它,但它肯定不是现实的错误发生。