我试图理解箭头符号,特别是它如何与 Monads 一起工作。使用 Monads,我可以定义以下内容:
f = (*2)
g = Just 5 >>= (return . f)
并且g
是Just 10
我该如何做上述但使用箭头符号?
翻译成 Arrow 的第一步是从m b
独立思考转向思考a -> m b
。
有了一个单子,你会写
use x = do
.....
....
doThis = do
....
...
thing = doThis >>= use
而箭头总是有输入,所以你必须这样做
doThis' _ = do
.....
....
然后使用(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
from Control.Monad
do have
thing' = doThis' >=> use
>=>
消除 的不对称性>>=
,这就是我们所说的 Monad 的 Kleisli 箭头。
()
输入或“如果我的第一件事真的不是函数怎么办?”没关系,如果你的 monad 不产生任何东西(比如 putStrLn 不产生任何东西),这只是一个共同问题,那么你只需要把它变成return ()
.
如果您的东西不需要任何数据,只需将其()
设为一个作为参数的函数。
doThis () = 做 .... ...
这样任何东西都有签名a -> m b
,你可以用>=>
.
箭头有签名
Arrow a => a b c
这可能不如中缀清楚
Arrow (~>) => b ~> c
但是您仍然应该将其视为类似于b -> m c
.
主要区别在于,b -> m c
您将您b
作为函数的参数,并且可以使用它做您喜欢的事情,就像if b == "war" then launchMissiles else return ()
使用箭头一样,但您不能(除非它是 ArrowApply - 请参阅此问题,了解为什么 ArrowApply 为您提供 Monad 功能) -一般来说,箭头只是做它所做的事情,而不是根据数据进行切换操作,有点像 Applicative 做的事情。
问题b -> m c
在于,您不能在实例声明中部分应用它以-> m
从中间获取位,因此假设它b -> m c
被称为 Kleisli 箭头,Control.Monad
定义(>>>)
为在所有包装和展开之后,您会得到f >>> g
= \x -> f x >>= g
- 但这相当于(>>>) = (>=>)
。(实际上(.)
是为 Categories 定义的,而不是 forwards composition >>>
,但我确实说等价!)
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }
instance Monad m => Category (Kleisli m) where
id = Kleisli return
(Kleisli f) . (Kleisli g) = Kleisli (\b -> g b >>= f) -- composition of Kleisli arrows
instance Monad m => Arrow (Kleisli m) where
arr f = Kleisli (return . f)
first (Kleisli f) = Kleisli (\ ~(b,d) -> f b >>= \c -> return (c,d))
second (Kleisli f) = Kleisli (\ ~(d,b) -> f b >>= \c -> return (d,c))
(尝试忽略所有Kleisli
和runKleisli
- 它们只是包装和展开单子值 - 当您定义自己的箭头时,它们不是必需的。)
如果我们解开这对 的含义Maybe
,我们会得到等价于 compose
f :: a -> Maybe b
g :: b -> Maybe c
f >>> g :: a -> Maybe c
f >>> g = \a -> case f a of -- not compilable code!
Nothing -> Nothing
Just b -> g b
并且应用(纯)函数的箭头方式是arr :: Arrow (~>) => (b -> c) -> b ~> c
我将修复(~->)
它的意思Kleisli Maybe
,以便您可以看到它的实际效果:
{-# LANGUAGE TypeOperators #-}
import Control.Arrow
type (~->) = Kleisli Maybe
g :: Integer ~-> Integer
g = Kleisli Just >>> arr (*2)
给予
ghci> runKleisli g 10
Just 20
do
符号一样,但有输入和输出。(GHC)GHC 实现了等价的do
notation,proc
notation,它可以让你做
output <- arrow -< input
你已经习惯了,output <- monad
但现在有了arrow -< input
符号。就像 Monads 一样,你不会<-
在最后一行做,你也不会在proc
符号中做。
让我们使用 tail 和 read from safe的 Maybe 版本来说明符号(和做广告safe
)。
{-# LANGUAGE Arrows #-}
import Control.Arrow
import Safe
this = proc inputList -> do
digits <- Kleisli tailMay -< inputList
number <- Kleisli readMay -<< digits
arr (*10) -<< number
请注意,我使用了 的-<<
变体-<
,它允许您通过将 左侧的内容<-
带入 右侧的范围来使用输出作为输入-<
。
显然this
等同于Kleisli tailMay >>> Kleisli readMay >>> arr (*10)
,但它只是 (!) 给你的想法。
ghci> runKleisli this "H1234" -- works
Just 1234
ghci> runKleisli this "HH1234" -- readMay fails
Nothing
ghci> runKleisli this "" -- tailMay fails
Nothing
ghci> runKleisli this "10" -- works
Just 0
()
就像我说的,()
如果我们没有输入,我们就使用它,就像我们在 Monad 中所做的那样,如果我们不需要输出任何东西,就返回它。
您还将()
在proc
符号示例中看到:
thing = proc x -> do
this <- thing1 -< ()
() <- thing2 -< x
returnA -< this
Maybe
首先,我们需要一个与monad具有相同语义的箭头。我们可以从头开始定义它,但最简单的方法是将Maybe
monad 包装成Kleisli
:
type MaybeArrow = Kleisli Maybe
然后我们还需要一种方法来运行这个 monad 来提取结果:
runMaybeArrow :: MaybeArrow () a -> Maybe a
runMaybeArrow = flip runKleisli ()
此外,有一种方法可以方便地从给定值创建一个常量箭头(它只是忽略它的输入):
val :: (Arrow a) => c -> a b c
val = arr . const
最后,我们得到:
g' = runMaybeArrow (val 5 >>> arr f)