Haskell的新手,我正试图找出这个Monad的东西。 monadic绑定运算符 - >>=
- 具有非常奇特的类型签名:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
为简化起见,我们将Maybe
替换为m
:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
但请注意, 定义可以用三种不同的方式编写:
(>>=) :: Maybe a -> (Maybe a -> Maybe b) -> Maybe b
(>>=) :: Maybe a -> ( a -> Maybe b) -> Maybe b
(>>=) :: Maybe a -> ( a -> b) -> Maybe b
在三者中,中心的一个是最不对称的。但是,据我所知,如果我们想要避免(LYAH称之为样板代码),第一个有点无意义。但是,接下来的两个,我更喜欢最后一个。对于Maybe
,这看起来像:
当定义为:
时(>>=) :: Maybe a -> (a -> b) -> Maybe b
instance Monad Maybe where
Nothing >>= f = Nothing
(Just x) >>= f = return $ f x
这里,a -> b
是一个普通的函数。此外,我没有立即看到任何不安全的东西,因为Nothing
在函数应用程序之前捕获了异常,因此除a -> b
之外不会调用Just a
函数得到了。
所以也许有些事情对我来说并不明显,这导致(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
定义比更简单的(>>=) :: Maybe a -> (a -> b) -> Maybe b
定义更受欢迎?是否存在与(我认为是)更简单的定义相关的一些固有问题?
答案 0 :(得分:22)
如果您根据以下派生函数(来自Control.Monad
)来考虑它会更加对称:
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
(f >=> g) x = f x >>= g
这个功能很重要的原因是它遵循三个有用的等式:
-- Associativity
(f >=> g) >=> h = f >=> (g >=> h)
-- Left identity
return >=> f = f
-- Right identity
f >=> return = f
这些是category laws,如果您将其翻译为使用(>>=)
而不是(>=>)
,则会获得三个monad法则:
(m >>= g) >>= h = m >>= \x -> (g x >>= h)
return x >>= f = f x
m >>= return = m
所以它不是(>>=)
优雅的运算符,而是(>=>)
是您正在寻找的对称运算符。但是,我们通常根据(>>=)
进行思考的原因是因为这是do
符号所荒废的原因。
答案 1 :(得分:8)
让我们考虑一下Maybe
monad的常见用法:处理错误。说我想安全地划分两个号码。我可以写这个函数:
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv n d = n `div` d
然后使用标准Maybe
monad,我可以做这样的事情:
foo :: Int -> Int -> Maybe Int
foo a b = do
c <- safeDiv 1000 b
d <- safeDiv a c -- These last two lines could be combined.
return d -- I am not doing so for clarity.
请注意,在每个步骤中,safeDiv
都可能失败,但在这两个步骤中,safeDiv
需要Int
次,而不是Maybe Int
次。如果>>=
有此签名:
(>>=) :: Maybe a -> (a -> b) -> Maybe b
你可以一起编写函数,然后给它一个Nothing
或Just
,要么打开Just
,要遍历整个管道,然后重新包装它在Just
中,或者只是通过Nothing
基本上不受影响。这可能有用,但它不是monad。为了它有用,我们必须能够在中间失败,这就是这个签名给我们带来的:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
顺便说一下,你设计的签名确实存在:
flip fmap :: Maybe a -> (a -> b) -> Maybe b
答案 2 :(得分:3)
使用a -> Maybe b
的更复杂的函数是更通用和更有用的函数,可用于实现简单的函数。这反过来不起作用。
您可以从函数a -> Maybe b
构建f :: a -> b
函数:
f' :: a -> Maybe b
f' x = Just (f x)
或者,就return
而言(Just
为Maybe
):
f' = return . f
反过来不一定是可能的。如果你有一个函数g :: a -> Maybe b
并希望将它与“简单”绑定一起使用,则必须先将其转换为函数a -> b
。但这通常不起作用,因为g
可能会返回Nothing
,其中a -> b
函数需要返回b
值。
所以一般来说,“简单”绑定可以用“复杂”绑定来实现,但不是相反。另外,复杂的绑定通常是有用的,没有它会使许多事情变得不可能。因此,通过使用更通用的绑定monad适用于更多情况。
答案 3 :(得分:2)
(>>=)
的替代类型签名的问题在于,它只是偶然适用于Maybe monad,如果你尝试使用另一个monad(即List monad),你会看到它崩溃了一般情况下b
的类型。你提供的签名并没有描述一元的约束,而且monad法律也不能坚持这个定义。
import Prelude hiding (Monad, return)
-- assume monad was defined like this
class Monad m where
(>>=) :: m a -> (a -> b) -> m b
return :: a -> m a
instance Monad Maybe where
Nothing >>= f = Nothing
(Just x) >>= f = return $ f x
instance Monad [] where
m >>= f = concat (map f m)
return x = [x]
因类型错误而失败:
Couldn't match type `b' with `[b]'
`b' is a rigid type variable bound by
the type signature for >>= :: [a] -> (a -> b) -> [b]
at monadfail.hs:12:3
Expected type: a -> [b]
Actual type: a -> b
In the first argument of `map', namely `f'
In the first argument of `concat', namely `(map f m)'
In the expression: concat (map f m)
答案 4 :(得分:1)
使monad成为monad的原因是如何加入&#39;作品。回想一下,join的类型为:
join :: m (m a) -> m a
什么&#39;加入&#39;确实是&#34;解释&#34; monad动作,根据monad动作返回monad动作。所以,你可以想到它剥掉了一层monad(或者更好的是,将内层的东西拉到外层)。这意味着&#39;&#39;&#39;形成一个&#34;堆栈&#34;,在&#34;调用堆栈&#34;的意义上。每个&#39; m&#39;代表一个背景,并加入&#39;让我们按顺序一起加入上下文。
那么,这与bind有什么关系?召回:
(>>=) :: m a -> (a -> m b) -> m b
现在考虑对f :: a - &gt; m b,和ma :: m a:
fmap f ma :: m (m b)
即,将f直接应用于a中的a的结果是(m(m b))。我们可以对此进行连接,以获得m b。简而言之,
ma >>= f = join (fmap f ma)