代码之家  ›  专栏  ›  技术社区  ›  qwe

Control.Lens不是多余的吗。要在函数中包装类型的设置程序?

  •  21
  • qwe  · 技术社区  · 8 年前

    我在看 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 }
    

    通过使用 控制镜头 ,同样的效果通过以下方式实现:

    over x (+1) p
    

    其中:

    x :: Functor f => (Int -> f Int) -> Point -> f Point
    over :: Setter Point Point Int Int -> (Int -> Int) -> Point -> Point
    

    所以我的问题是,既然同样的效果可以用更简单的方式实现,为什么 控制镜头 用函子包装东西?在我看来这是多余的,因为 xlens公司 控制镜头 over 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)
    
    2 回复  |  直到 8 年前
        1
  •  20
  •   hao    8 年前

    这是一个很好的问题,需要一点解包。

    我想马上纠正你一点: lens 最新版本的包是

    type Setter s t a b = (a -> Identity b) -> s -> Identity t
    

    Functor 现在……还没有。

    然而,这并不会使你的问题无效。为什么类型不简单

    type Setter s t a b = (a -> b) -> s -> t
    

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

    透镜

    A. 透镜 是一种允许我们同时执行getter和setter操作的类型。这两个组合形成了一个漂亮的功能参考。

    简单的选择 透镜 类型为:

    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)
    

    然而,这种类型非常令人不满意。

    • 它无法与 . ,这可能是 透镜 包裹
    • 构建大量元组会导致内存效率低下,只会在稍后将其拆开。
    • 最大的问题是:需要getter的函数(比如 view )和设置器(如 over )不能带隐形眼镜,因为它们的类型太不同了。

    如果最后一个问题没有解决,为什么还要写一个库呢?我们不希望用户必须不断思考自己在UML光学层次结构中的位置,每次上下移动时都要调整其函数调用。

    现在的问题是:有没有一种类型可以写下来 透镜 就是这样 自动地 两者都是 Getter 和a Setter ? 为此,我们必须改变 Getter公司 设置器 .

    Getter公司

    • 首先要注意的是 s -> a 等于 forall r. (a -> r) -> s -> r 。这种向接续传球风格的转变并不明显。您可以直观地理解这种转换:“类型的函数 s->一 是一个承诺 s 你可以递给我一个 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->一 ).

    设置器

    • 定义 newtype Identity a = Identity a 。请注意 Identity a 数学上和运行时。

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

    所有人一起

    有了这些文书工作,我们可以将setter和getter统一为一个吗 透镜 类型

    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
    

    这是Haskell,我们可以抽象出 Identity Const 量化变量。像 the lens wiki says ,所有这些 施工 身份 共同点是每一个都是 Functor公司 。然后,我们选择它作为这些类型的某种统一点:

    type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
    

    (还有其他选择的理由 Functor公司 也可以用自由定理证明函数参考定律。但我们会在这里节省一点时间。)那个 forall f 就像 forall r 它允许类型的使用者选择如何填充变量。填写 身份 你会得到一个二传手。填写一个 Const a 你会得到一个吸气剂。正是通过在这一过程中选择小而仔细的转换,我们才能够达到这一点。

    注意事项

    需要注意的是,这个推导是 最初的动机 透镜 包裹作为 Derivation wiki page states 解释道,你可以从以下有趣的行为开始 (.) 具有某些功能,并从那里梳理出光学。但我认为,我们开辟的这条道路在解释你提出的问题方面稍微好一些,这也是我最初提出的一个大问题。我还想介绍你 lens over tea ,它还提供 另一个 起源

    我认为这些多重衍生产品是一件好事,也是一种衡量健康的试金石 透镜 设计我们能够从不同的角度得出相同的优雅解决方案,这意味着这种抽象是健壮的,并且得到了不同直觉和数学的良好支持。

    我最近还对Setter的类型撒了一点谎 透镜 .实际上

    type Setter s t a b = forall f. Settable f => (a -> f b) -> s -> t b
    

    这是另一个将高阶类型抽象为光学类型的示例,以便为库用户提供更好的体验。几乎总是这样 f 将被实例化为 身份 ,因为有一个 instance Settable Identity 。但是,您可能不时希望将setter传递给 backwards 函数,修复 f 成为 Backwards Identity 。我们可以将此段落归类为“有关 透镜 而不是你想知道的。”

        2
  •  6
  •   leftaroundabout    8 年前

    在某种意义上,原因 lens 在functor返回中包装setter是 太强大了 否则

    事实上,当setter 习惯于 ,函子将被实例化为 Identity 无论如何,这和你提议的签名完全一样。然而 setter的实现不能利用这个事实 .有了你的签名,我可以写下如下内容

    zlens :: MySetter Point Point Int Int
    zlens _f p = p  -- no z here!
    

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

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

    更实际地说,我们需要的是 兼容性 。虽然您可以定义 设置器 没有这个包装器,getter就不可能这样做,因为 Const 而不是 身份 ,并需要在此类型构造函数的第一个参数中添加多态性。通过要求这样的包装器 全部的 镜头风格(仅限于不同的类约束),我们可以对所有镜头使用相同的组合词,但类型系统总是将功能分解为实际适用于该情况的任何功能。


    想想看,这种保证实际上不是很有力……我仍然可以用一些东西来颠覆它 fmap (const old) 作弊,但这肯定不是真正可能发生的错误。