在>> = Monad运算符的签名上

时间:2014-01-19 19:43:39

标签: haskell monads

这是Haskell

中众所周知的>> =运算符的签名
>>= :: Monad m => m a -> (a -> m b) -> m b

问题是为什么函数的类型是

(a -> m b)

而不是

(a -> b)

我想说后者更实用,因为它允许在定义的monad中直接集成现有的“纯”函数。

相反,编写一般的“适配器”似乎并不困难

adapt :: (Monad m) => (a -> b) -> (a -> m b)

但无论如何我认为你已经有(a -> b)而不是(a -> m b)的可能性更大。

注意。我用“pratical”和“probable”来解释我的意思。 如果你还没有在程序中定义任何monad,那么,你拥有的函数是“纯粹的”(a -> b),你将拥有0 (a -> m b)类型的函数,因为你还没有定义{ {1}}。如果您决定定义monad m,则需要定义新的m函数。

6 个答案:

答案 0 :(得分:9)

原因是(>>=)更为一般。您建议的功能称为liftM,可以轻松定义为

liftM :: (Monad m) => (a -> b) -> (m a -> m b)
liftM f k  =  k >>= return . f

此概念有自己的类型类Functor,带有fmap :: (Functor m) => (a -> b) -> (m a -> m b)。每个Monad也是Functor fmap = liftM,但由于历史原因,这不是(yet)在类型层次结构中捕获的。

您建议的adapt可以定义为

adapt :: (Monad m) => (a -> b) -> (a -> m b)
adapt f = return . f

请注意,adapt等同于return return可以定义为adapt id

所有具有>>=的东西也可以具有这两个功能,但反之亦然。 There are structures that are Functors but not Monads.

这种差异背后的直觉很简单:monad中的计算可能取决于之前monad的结果。重要的部分是(a -> m b),这意味着不仅b,而且其“效果”m b也可能取决于a。例如,我们可以

import Control.Monad

mIfThenElse :: (Monad m) => m Bool -> m a -> m a -> m a
mIfThenElse p t f = p >>= \x -> if x then t else f

但仅使用Functor m仅用fmap约束来定义此函数是不可能的。函子只允许我们改变“内部”的值,但是我们不能“退出”来决定采取什么行动。

答案 1 :(得分:7)

基本上,(>>=)允许您对操作进行排序,以便后面的操作可以根据早期结果选择不同的行为。您可以在Functor类型类中使用更为纯粹的函数,可以使用(>>=)导出,但如果单独使用它,则根本无法对操作进行排序。还有一个名为Applicative的中间体,它允许您对操作进行排序,但不会根据中间结果更改它们。

举个例子,让我们从Functor到Monad构建一个简单的IO动作类型。


我们将专注于GetC类型,如下所示

GetC a = Pure a | GetC (Char -> GetC a)

第一个构造函数会及时有意义,但第二个构造函数应该立即生效 - GetC包含一个可以响应传入字符的函数。我们可以将GetC转换为IO操作,以便提供这些字符

io :: GetC a -> IO a
io (Pure a)  = return a
io (GetC go) = getChar >>= (\char -> io (go char))

这清楚地说明Pure来自哪里---它处理我们类型中的纯值。最后,我们将使GetC抽象:我们将不允许直接使用PureGetC,并允许我们的用户仅访问我们定义的函数。我现在写下最重要的一个

getc :: GetC Char
getc = GetC Pure

然后立即考虑获得角色的函数是纯值。虽然我把它称为最重要的功能,但很明显,现在GetC是无用的。我们所能做的只是运行getc,然后运行io ...以获得完全等同于getChar的效果!

io getc        ===     getChar     :: IO Char

但我们会从这里建立。


如开头所述,Functor类型类提供的函数与您正在查找的函数fmap完全相同。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

事实证明,我们可以将GetC实例化为Functor,所以让我们这样做。

instance Functor GetC where
  fmap f (Pure a)  = Pure (f a)
  fmap f (GetC go) = GetC (\char -> fmap f (go char))

如果您眯着眼睛,您会注意到fmap仅影响Pure构造函数。在GetC构造函数中,它只是“被推下”并推迟到以后。这是对fmap的弱点的暗示,但让我们试试吧。

io                       getc  :: IO Char
io (fmap ord             getc) :: IO Int
io (fmap (\c -> ord + 1) getc) :: IO Int

我们已经能够修改我们类型的IO解释的返回类型,但就是这样!特别是,我们仍然只能获得一个角色,然后回到IO做任何有趣的事情。

这是Functor的弱点。因为,正如你所指出的那样,它仅处理纯函数,它“在计算结束时”仅仅修改Pure构造函数。“


下一步是Applicative,它扩展Functor就像这样

class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

换句话说,它扩展了将纯值注入我们的上下文的概念,允许纯函数 application 跨越数据类型。不出所料,GetC实例化Applicative

instance Applicative GetC where
  pure = Pure
  Pure f   <*> Pure x   = Pure (f x)
  GetC gof <*> getcx    = GetC (\char -> gof <*> getcx)
  Pure f   <*> GetC gox = GetC (\char -> fmap f (gox char))

Applicative允许我们对操作进行排序,并且可能已从定义中清楚地看出。实际上,我们可以看到(<*>)推送字符应用程序,以便GetC任意一侧的(<*>)操作按顺序执行。我们像这样使用Applicative

fmap (,) getc <*> getc :: GetC (Char, Char)

它允许我们构建令人难以置信的有趣功能,比Functor更复杂。例如,我们已经可以形成一个循环并获得无限的字符流。

getAll :: GetC [Char]
getAll = fmap (:) getc <*> getAll

演示了Applicative能够一个接一个地对动作进行排序的性质。

问题在于我们无法阻止。 io getAll是一个无限循环,因为它只是永远消耗字符。例如,我们无法告诉它在看到'\n'时停止,因为Applicative的序列没有注意到之前的结果。


让我们走最后一步实例化Monad

instance Monad GetC where
  return = pure
  Pure a  >>= f = f a
  GetC go >>= f = GetC (\char -> go char >>= f)

这使我们能够立即实施停止getAll

getLn :: GetC String
getLn = getc >>= \c -> case c of
  '\n' -> return []
  s    -> fmap (s:) getLn

或者,使用do表示法

getLn :: GetC String
getLn = do
  c <- getc
  case c of
    '\n' -> return []
    s    -> fmap (s:) getLn

那是什么给出的?为什么我们现在可以写一个停止循环?

因为(>>=) :: m a -> (a -> m b) -> m b允许第二个参数(纯值的函数)选择下一个操作m b。在这种情况下,如果传入的字符是'\n',我们选择return []并终止循环。如果没有,我们选择递归。

这就是为什么你可能希望Monad超过Functor。这个故事还有很多,但这些都是基础知识。

答案 2 :(得分:1)

正如其他人所说,你的绑定是fmap类的Functor函数,a.k.a <$>

但为什么它不如>>=强大?

  

编写一般的“适配器”似乎并不困难

adapt :: (Monad m) => (a -> b) -> (a -> m b)

你确实可以用这种类型写一个函数:

adapt f x = return (f x)

但是,这个函数不能完成我们可能需要>>=的参数。 adapt无法生成有用的值。

在列表monad return x = [x]中,adapt将始终返回单个元素列表。

Maybe monad return x = Some x中,adapt永远不会返回None

IO monad中,一旦你检索到一个操作的结果,你所能做的只是从中计算一个新值,你不能运行后续操作!

等。简而言之,fmap能够比>>=做更少的事情。这并不意味着它没用 - 它不会有一个名字,如果它是:)但它不那么强大。

答案 3 :(得分:0)

monad的整个“点”(将其置于算子或应用之上)是您可以根据左侧的值/结果确定您“返回”的monad。

例如,>>=类型的Maybe允许我们决定返回Just xNothing。您会注意到使用仿函数或应用程序,根据“已排序”的“可能”,无法“选择”返回Just xNothing

尝试实施以下内容:

halve :: Int -> Maybe Int
halve n | even n    = Just (n `div` 2)
        | otherwise = Nothing

return 24 >>= halve >>= halve >>= halve

只有<$>fmap1)或<*>ap)。

实际上,您提到的“纯代码的直接集成”是functor design pattern的一个重要方面,并且非常有用。然而,它在很多方面与>>=背后的动机无关 - 它们适用于不同的应用程序和事物。

答案 4 :(得分:0)

一段时间以来,我有一个相同的问题,并且在思考为什么一旦将a -> m b映射到a -> b时就对m a -> m b感到困扰,这看起来更加自然。这很像在问“为什么我们需要一个提供函子的monad”。

我给自己的一个小答案是a -> m b会说明您无法使用函数a -> b捕获的副作用或其他复杂性。

更好的措词形式here(强烈推荐):

一元值 M a本身可以看作是计算。一元函数表示以某种方式是非标准的计算,即编程语言自然不支持的计算。这可能意味着纯功能语言的副作用或不纯功能语言的异步执行。普通函数类型无法对此类计算进行编码,而是使用具有单子结构的数据类型进行编码。

我会强调普通函数类型不能编码这样的计算,其中普通是a -> b

答案 5 :(得分:-1)

我认为J. Abrahamson的回答指出了正确的理由:

  

基本上,(&gt;&gt; =)允许您对操作进行排序,以便后面的操作可以根据之前的结果选择不同的行为。您可以使用Functor类型类中提供的更纯粹的函数,并且可以使用(&gt;&gt; =)导出,但如果您单独使用,则根本无法对所有操作进行排序< / strong>即可。

让我向>>= :: Monad m => m a -> (a -> b) -> m b展示一个简单的反例。

很明显,我们希望将值绑定到上下文。也许我们需要在这些“上下文值”上依次链接函数。 (这只是Monads的一个用例)。

Maybe简单地视为“上下文值”的情况。

然后定义一个“假”monad类:

class Mokad m where  
     returk :: t -> m t  
     (>>==) :: m t1 -> (t1 -> t2) -> m t2

现在让我们尝试让Maybe成为Mokad

的实例
instance Mokad Maybe where
         returk x = Just x
         Nothing >>== f = Nothing
         Just x >>== f = Just (f x) -- ????? always Just ?????

出现第一个问题:>>==始终返回Just _

现在让我们尝试使用Maybe将功能链接到>>== (我们按顺序提取三个Maybe的值只是为了添加它们)

chainK :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
chainK ma mb mc = md 
      where
        md = ma >>== \a -> mb >>== \b -> mc >>== \c -> returk $ a+b+c

但是,此代码无法编译:{{1​​}}类型为md,因为每次使用Maybe (Maybe (Maybe Int))时,它都会将之前的结果封装到>>==框中。< / p>

恰恰相反,Maybe工作正常:

>>=