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

qwe*_*qwe 20 haskell functor applicative lenses haskell-lens

我正在观看Control.Lens介绍视频.
这让我想知道为什么Setter类型需要在仿函数中包装它.
它(大致)定义如下:

type Control.Lens.Setter s t a b = (Functor f) => (a -> f a) -> s -> f t
Run Code Online (Sandbox Code Playgroud)

假设我有一个名为Pointthis的定义数据:

data Point = Point { _x :: Int, _y :: Int } deriving Show
Run Code Online (Sandbox Code Playgroud)

然后我可以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) }
Run Code Online (Sandbox Code Playgroud)

我可以像这样使用它:

p = Point 100 200
xlens (+1) p -- Results in Point { _x = 101, _y = 200 }
Run Code Online (Sandbox Code Playgroud)

通过使用Control.Lens,通过以下方式实现相同的效果:

over x (+1) p
Run Code Online (Sandbox Code Playgroud)

以下是:

x :: Functor f => (Int -> f Int) -> Point -> f Point
over :: Setter Point Point Int Int -> (Int -> Int) -> Point -> Point
Run Code Online (Sandbox Code Playgroud)

所以我的问题是,既然可以用更简单的方式实现同​​样的效果,为什么要用Control.Lens仿函数包装东西呢?它看起来多余的给我,因为我xlens不一样Control.Lensover x.

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

data Atom = Atom { _element :: String, _pos :: Point } deriving Show
poslens :: MySetter Atom Atom Point Point
poslens f a = a { _pos = f (_pos a) }

a = Atom "Oxygen" p
(poslens . xlens) :: (Int -> Int) -> Atom -> Atom
(poslens . xlens) (+1) a -- Results in Atom "Oxygen" (Point 101 200)
Run Code Online (Sandbox Code Playgroud)

hao*_*hao 19

这是一个很好的问题,需要一点点拆包.

我想在一个点上轻轻地纠正你:lens最近版本的包装中Setter的类型是

type Setter s t a b = (a -> Identity b) -> s -> Identity t
Run Code Online (Sandbox Code Playgroud)

没有Functor迹象...但.

但这并不会使您的问题无效.为什么不是简单的类型

type Setter s t a b = (a -> b) -> s -> t
Run Code Online (Sandbox Code Playgroud)

为此,我们首先要谈谈Lens.

镜片

A 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)
Run Code Online (Sandbox Code Playgroud)

然而,这种类型非常不满意.

  • 它无法构成.,这可能是该lens软件包的唯一最佳卖点.
  • 构建大量元组是相当低效的内存,只是为了以后将它们分开.
  • 最重要的一点:吸取(如view)和设置者(如over)的功能不能拍摄镜头,因为它们的类型是如此不同.

如果没有最后一个问题解决了为什么甚至打扰写一个库?我们讨厌用户不得不经常考虑它们在UML光学层次结构中的位置,每次上下移动时调整它们的函数调用.

那时候的问题是:我们可以写下一种类型Lens,它是自动的 a Getter和a Setter?为此我们必须改变的类型GetterSetter.

消气

  • 首先注意s -> a相当于forall r. (a -> r) -> s -> r.这种转变为延续传递方式远非显而易见.你或许可以直觉这样的转换:"类型的函数s -> a是一个承诺,任何s你可以交给我一个a.但这应该等同于给定一个函数映射ar你的承诺可以交给我一个函数也映射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数学和运行时相同.

  • 现在请注意,type Setter s t a b = (a -> Identity b) -> s -> Identity t它仍然与我们开始使用的类型相同.

全部一起

通过这个文书工作,我们可以将setter和getter统一到一个单一的Lens类型中吗?

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
Run Code Online (Sandbox Code Playgroud)

嗯,这是Haskell和我们可以抽象出的选择IdentityConst以量化的变量.正如镜头维基所说,所有这一切Const并且Identity有一个共同点就是每个都是一个Functor.然后我们选择它作为这些类型的统一点:

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
Run Code Online (Sandbox Code Playgroud)

(还有其他理由可以选择Functor,例如通过使用自由定理来证明函数引用的定律.但是我们会在这里稍微移动一下.)forall f就像是forall r.上面 - 它允许该类型的消费者选择如何填充变量.填写一个Identity,你得到一个setter.填写一个Const a,你得到一个吸气剂.通过选择我们能够在此时到达的方式进行小而细致的转换.

注意事项

值得注意的是,这种推导并不是lens包的原始动机.正如衍生维基页面状态所解释的那样,您可以从(.)某些功能的有趣行为开始,并从那里取出光学元件.但是我认为我们制定的这条道路在解释你提出的问题时要好一点,这也是我开始提出的一个大问题.我也想把你推荐给镜头茶,这提供了另一种推导.

我认为这些多重推导是一件好事,也是lens设计健康性的一种量油尺.我们能够从不同角度得出同样优雅的解决方案,这意味着这种抽象是强大的,并得到不同直觉和数学的良好支持.

我最近也对Setter的类型撒谎了一点lens.实际上

type Setter s t a b = forall f. Settable f => (a -> f b) -> s -> t b
Run Code Online (Sandbox Code Playgroud)

这是抽象光学类型中的高阶类型的另一个例子,以便为图书馆用户提供更好的体验.几乎总是f会被实例化Identity,因为有一个instance Settable Identity.但是每一个现在,那么你可能要制定者传递给backwards函数,修复fBackwards Identity.我们可以将这一段分类为" lens您可能想知道的更多信息".


lef*_*out 6

从某种意义上说,lens在函子返回中包装setter 的原因是它们会过于强大.

事实上,当一个setter 使用,仿函数将被实例Identity无论如何,这正是喜欢你提出的签名.但是,setter的实现不得利用这一事实.有了你的签名,我可以写出类似的东西

zlens :: MySetter Point Point Int Int
zlens _f p = p  -- no z here!
Run Code Online (Sandbox Code Playgroud)

嗯,这是Functor基于签名不可能的,因为zlens需要通过仿函数进行普遍量化,它不知道如何将结果注入f包装器.获得仿函数类型结果的唯一方法是首先将setter函数应用于正确类型的字段!

所以,这只是一个很好的自由定理.

更实际的是,我们需要functor包装器以实现兼容性.虽然您可以在没有此包装器的情况下定义setter,但这对于getter来说是不可能的,因为这些使用Const而不是Identity,并且需要在此类型构造函数的第一个参数中添加多态性.通过要求所有镜片口味的包装(仅具有不同的类别限制),我们可以为所有镜片使用相同的组合器,但类型系统将始终将功能折叠到实际适用于该情况的任何功能.


考虑到这一点,保证实际上并不是很强大......我仍然可以用一些fmap (const old)骗子来颠覆它,但它肯定不会真实地发生错误.