这是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
函数。
答案 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
抽象:我们将不允许直接使用Pure
或GetC
,并允许我们的用户仅访问我们定义的函数。我现在写下最重要的一个
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 x
或Nothing
。您会注意到使用仿函数或应用程序,根据“已排序”的“可能”,无法“选择”返回Just x
或Nothing
。
尝试实施以下内容:
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
工作正常:
>>=