为什么在monad中使用这种特殊的函数类型?

时间:2014-04-14 04:08:08

标签: haskell types monads typeclass

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定义更受欢迎?是否存在与(我认为是)更简单的定义相关的一些固有问题?

5 个答案:

答案 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

你可以一起编写函数,然后给它一个NothingJust,要么打开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而言(JustMaybe):

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)