例如,可以使用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
?还有其他用例吗? 最佳答案
好吧,在这种情况下,您可以手工完成融合,因为两个fmap
在源代码中是“可见的”,但要点是Yoneda
在运行时进行了转换。这是一个动态的事情,当您不知道在结构上需要fmap
多少次时,它很有用。例如。考虑lambda术语:
data Term v = Var v | App (Term v) (Term v) | Lam (Term (Maybe v))
Maybe
下的Lam
表示受抽象限制的变量;在Lam
的主体中,变量Nothing
指的是绑定(bind)变量,所有变量Just v
表示环境中绑定(bind)的变量。 (>>=) :: Term v -> (v -> Term v') -> Term v'
表示替换-可以将每个v
ariable替换为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
。如果我将Var v
掩埋在1000 Lam
下,那么我最终会调用fmap Just
并在新的f v
术语上迭代1000次!我不能随便用手把多个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
都只被调用一次,这意味着例如Var v >>= f = fmap id (f v)
以前只是f v
。)关于haskell - 从理论的角度来看,Yoneda Lemma仅有用吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/56477752/