对于Control.Lens.Setter来说,在函子中包装类型不是多余的吗?

时间:2016-06-06 20:57:53

标签: haskell functor applicative lens lenses

我在看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将事物包装在仿函数中呢?这看起来很多,因为我的xlensControl.Lens的{​​{1}}相同。

只是为了记录,我也可以用同样的方式链接我的镜头:

over x

2 个答案:

答案 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} }?为此,我们必须转换SetterGetter的类型。

吸气剂

  • 首先请注意,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 saysConstConst的共同点是每个都是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骗子来颠覆它,但它肯定不是现实的错误发生。