例如,可以使用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
?还有其他用例吗?
答案 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
等),通常会优化所涉及的函子(通常为over
或Const
)。但是,在少数情况下,是不可能做到这一点的(例如,如果使用镜头的方式是在编译时未对函子进行具体选择)。作为补偿, 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
的融合。