从理论的角度来看,Yoneda Lemma仅有用吗?

时间:2019-06-06 12:37:02

标签: haskell functor function-composition

例如,可以使用Yoneda获得循环融合:

newtype Yoneda f a =
    Yoneda (forall b. (a -> b) -> f b)

liftYo :: (Functor f) => f a -> Yoneda f a
liftYo x = Yoneda $ \f -> fmap f x

lowerYo :: (Functor f) => Yoneda f a -> f a
lowerYo (Yoneda y) = y id

instance Functor (Yoneda f) where
    fmap f (Yoneda y) = Yoneda $ \g -> y (g . f)

loopFusion = lowerYo . fmap f . fmap g . liftYo

但是我本来可以写loopFusion = fmap (f . g)。为什么要完全使用Yoneda?还有其他用例吗?

2 个答案:

答案 0 :(得分:12)

好吧,在这种情况下 您可以手工完成融合,因为两个fmap在源代码中是“可见的”,但要点是{{1}在运行时进行转换。这是一个动态的事情,当您不知道不知道要在结构上Yoneda进行多少次时最有用。例如。考虑lambda条款:

fmap

data Term v = Var v | App (Term v) (Term v) | Lam (Term (Maybe v)) 下的Maybe代表受抽象约束的变量;在Lam主体中,变量Lam指的是绑定变量,所有变量Nothing代表环境中绑定的变量。 Just v表示替换-每个(>>=) :: Term v -> (v -> Term v') -> Term v'变量都可以替换为v。但是,在替换Term中的变量时,需要将产生的Lam中的所有变量包装在Term中。例如

Just

Lam $ Lam $ Var $ Just $ Just $ () >>= \() -> App (Var "f") (Var "x") = Lam $ Lam $ App (Var $ Just $ Just "f") (Var $ Just $ Just "x") 的简单实现是这样的:

(>>=)

但是,这样写,(>>=) :: Term v -> (v -> Term v') -> Term v' Var x >>= f = f x App l r >>= f = App (l >>= f) (r >>= f) Lam b >>= f = Lam (b >>= maybe (Var Nothing) (fmap Just . f)) 下的每个Lam都会向(>>=)添加fmap Just。如果我有一个f埋在1000 Var v下,那么我最终会打电话给Lam并遍历新的fmap Just项1000次!我不能随便用手把多个f v融合在一起,因为在源代码中只有一个fmap被多次调用。

fmap可以减轻痛苦:

Yoneda

现在,bindTerm :: Term v -> (v -> Yoneda Term v') -> Term v' bindTerm (Var x) f = lowerYoneda (f x) bindTerm (App l r) f = App (bindTerm l f) (bindTerm r f) bindTerm (Lam b) f = Lam (bindTerm b (maybe (liftYoneda $ Var Nothing) (fmap Just . f))) (>>=) :: Term v -> (v -> Term v') -> Term v' t >>= f = bindTerm t (liftYoneda . f) 是免费的;这只是一个奇怪的功能组成。生成的fmap Just上的实际迭代位于Term中,每个lowerYoneda仅调用一次。重申一下:源代码中无处包含Var形式的任何内容。这些形式仅在运行时动态出现,具体取决于fmap f (fmap g x)的参数。 (>>=)可以在运行时 将其重写为Yoneda,即使您不能像在源代码中那样将其重写。此外,您可以将fmap (f . g) x添加到现有代码中,而只需对其进行最少的更改。 (但是,有一个缺点:Yoneda对于每个lowerYoneda总是被调用一次,这意味着例如Var只是{{1} },之前。)

答案 1 :(得分:5)

有一个例子与 lens 中的the one described by HTNW相似。翻阅the lens function(改写为fusing)可以看到典型的van Laarhoven镜头的外观:

-- type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens getter setter = \f -> \s -> fmap (setter s) (f (getter s))

在那里fmap的出现意味着原则上构成镜头会导致fmap的连续使用。现在,在大多数情况下,这实际上并不重要: lens 中的实现使用了很多内联和新类型强制,因此,当使用 lens 组合器时,{{ 1}},view等),通常会优化所涉及的函子(通常为overConst)。但是,在少数情况下,是不可能做到这一点的(例如,如果使用镜头的方式是在编译时未对函子进行具体选择)。作为补偿, lens 提供了一个名为a comment by Edward Kmett的辅助函数,在某些特殊情况下,它可以使Identity融合。其实现是:

fmap

因此,如果您编写-- type LensLike f s t a b = (a -> f b) -> s -> f t fusing :: Functor f => LensLike (Yoneda f) s t a b -> LensLike f s t a b fusing t = \f -> lowerYoneda . t (liftYoneda . f) fusing (foo . bar)将被用作Yoneda f使用的函子,从而保证foo . bar的融合。

(此答案的灵感来自enter image description here,在碰到这个问题之前,我偶然发现了它。)