这是绑定方法的类型声明:
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
我按如下方式阅读:将一个返回包装值的函数应用于包装值。
此方法作为Monad类型类的一部分包含在Prelude中。这意味着有很多情况需要它。
好的,但我不明白为什么它是典型案例的典型解决方案。
如果您已经创建了一个返回包装值的函数,为什么该函数还没有包含值?
换句话说,有许多函数采用正常值但返回包装值的典型情况是什么? (而不是获取包装值并返回包装值)
答案 0 :(得分:11)
值的“展开”正是你在处理monad时想要隐藏的东西,因为这会导致很多样板。
例如,如果您有一系列操作返回要组合的Maybe
值,则必须手动传播Nothing
如果收到一个:
nested :: a -> Maybe b
nested x = case f x of
Nothing -> Nothing
Just r ->
case g r of
Nothing -> Nothing
Just r' ->
case h r' of
Nothing -> Nothing
r'' -> i r''
这就是bind为你做的事情:
Nothing >>= _ = Nothing
Just a >>= f = f a
所以你可以写:
nested x = f x >>= g >>= h >>= i
有些monad根本不允许你手动解压缩值 - 最常见的例子是IO
。从IO
获取值的唯一方法是map
或>>=
,这两种方法都要求您在输出中传播IO。
答案 1 :(得分:3)
每个人都专注于IO
monad并且无法“解开”。
但Monad
并非总是容器,因此您无法解包。
Reader r a == r->a such that (Reader r) is a Monad
我想到的是Monad
不是容器的最简单的最好例子。
您可以轻松编写一个可以生成m b
给定a
:a->(r->b)
的函数。但是你不能轻易地“展开”m a
中的值,因为a
没有包含在其中。 Monad是一种类型概念。
另请注意,如果您有m a->m b
,则表示您没有Monad
。 Monad
为您提供了一种构建来自m a->m b
的{{1}}函数的方式(比较:a->m b
为您提供了构建函数的方法来自Functor
的{{1}}; m a->m b
为您提供了一种从a->b
构建函数ApplicativeFunctor
的方法
答案 2 :(得分:2)
如果您已经创建了一个返回包装值的函数,为什么该函数还没有包含值?
因为该函数必须解包其参数才能对其执行某些操作。
但是对于m
的许多选择,如果最终会重新打包自己的结果,则只能打开一个值。这种“展开,做某事,然后重新包装”的想法体现在(>>=)
函数中,它为你解开,让你做某事,并强迫你按类型a -> m b
重新包装。
要理解为什么在没有最终重新打包的情况下无法打开包装,我们可以看一些例子:
如果m a = Maybe a
,对Just x
进行展开很容易:只需返回x
即可。但是我们如何解开Nothing
?我们不可以。但是如果我们知道我们最终会重新包装,我们可以跳过“做某事”步骤并返回Nothing
进行整体操作。
如果m a = [a]
,对[x]
进行展开很容易:只需返回x
即可。但是对于展开[]
,我们需要与Maybe a
相同的技巧。那么展开[x, y, z]
怎么样?如果我们知道我们最终将重新包装,我们可以执行“执行某些操作”三次,将x
,y
和z
以及concat
结果放入单个列表中
如果是m a = IO a
,那么解包很容易,因为我们实际上只在将来实际运行IO操作时才会知道结果。但是,如果我们知道我们最终将重新包装,我们可以在IO操作中存储“做某事”,并在我们执行IO操作时执行它。
我希望这些例子清楚地表明,对于m
的许多有趣的选择,如果我们知道我们要重新包装,我们只能实现解包。 (>>=)
的类型恰好允许这种假设,因此巧妙地选择使事情有效。
答案 3 :(得分:2)
我将专注于你的观点
如果您已经创建了一个返回包装值的函数,那么为什么 该功能是否已经采取包裹值?
和IO
monad。假设你有
getLine :: IO String
putStrLn :: IO String -> IO () -- "already takes a wrapped value"
如何编写一个读取一行并打印两次的程序?尝试将是
let line = getLine
in putStrLn line >> putStrLn line
但是等式推理要求这相当于
putStrLn getLine >> putStrLn getLine
改为读取两个行。
我们缺少的是一种解开"解开" getLine
一次,并使用两次。同样的问题适用于阅读一行,打印"你好",然后打印一行:
let line = getLine in putStrLn "hello" >> putStrLn line
-- equivalent to
putStrLn "hello" >> putStrLn getLine
所以,我们也没有办法指定"什么时候打开" getLine
。绑定>>=
运算符提供了一种方法。
如果交换(>>=)
绑定运算符周围的参数变为(=<<)
(=<<) :: (a -> m b) -> (m a -> m b)
将任何函数f
转换为包含函数g
的函数g
值。此类f
被称为{{1}}的{{3}}扩展名。绑定运算符保证
这样的扩展始终存在,并提供了一种使用它的便捷方式。
答案 4 :(得分:2)
虽然(>>=)
在直接使用时有时会很有用,但其主要目的是在 do notation 中实现<-
绑定语法。它的类型为m a -> (a -> m b) -> m b
,主要是因为,当在符号块中使用时,<-
的右侧属于m a
类型,左侧“绑定”a
1}}到给定的标识符,当与do块的其余部分组合时,类型为a -> m b
,结果monadic动作的类型为m b
,这是它可能具有的唯一类型使这项工作。
例如:
echo = do
input <- getLine
putStrLn input
<-
的右侧属于IO String
<-
的左侧与do块的其余部分属于String -> IO ()
类型。与使用>>=
:
echo = getLine >>= (\input -> putStrLn input)
>>=
的左侧是IO String
类型。右侧是String -> IO ()
类型。现在,通过将eta reduction应用于lambda,我们可以得到:
echo = getLine >>= putStrLn
显示了>>=
有时直接使用的原因,而不是“{1}}与”{1}}相关的“引擎”。
我还想提供我认为对“解开”monadic值的概念的一个重要修正,即它不会发生。 Monad类不提供类型>>
的通用函数。一些特定的实例做但这通常不是monad的一个特性。 Monads,一般来说,不能“解开”。
请记住,Monad m => m a -> a
是一项必须适用于任何monad的法律。 m >>= k = join (fmap k m)
的任何特定实现必须满足此法律,因此必须等同于此一般实现。
这意味着真正发生的是,monadic“计算”>>=
被“提升”成为a -> m b
使用fmap,然后应用m a -> m (m b)
,给出{ {1}};然后使用m a
将两个m (m b)
压缩在一起以产生join :: m (m a) -> m a
。因此,m
永远不会从“monad”中“消失”。 monad永远不会“打开”。这是一种考虑单子的错误方法,我强烈建议你不要养成习惯。
答案 5 :(得分:1)
因为我们希望能够将a -> b
等功能应用到我们的m a
。将此类函数提升为m a -> m b
是微不足道的(liftM, liftA
,>>= return .
,fmap
),但相反的情况不一定可能。
答案 6 :(得分:1)
你想要一些典型的例子吗? putStrLn :: String -> IO ()
怎么样?这个函数的类型为IO String -> IO ()
是没有意义的,因为字符串的来源无关紧要。
无论如何:由于你的“包裹价值”比喻,你可能有错误的想法;我经常使用它,但它有其局限性。从a
中获取m a
并不一定是纯粹的方式 - 例如,如果你有一个getLine :: IO String
,那么你可以用很多有趣的东西来做它 - 你可以将它放在一个列表中,将它连接起来以及其他整洁的东西,但是你无法从中获取任何有用的信息,因为你无法查看IO动作。你可以做的是使用>>=
,它可以让你使用行动的结果。
类似的东西适用于“包装”比喻也适用的单子;例如,点Maybe
monad是为了避免始终使用Just
手动包装和展开值。
答案 7 :(得分:1)
我最常见的两个例子:
1)我有一系列生成列表列表的函数,但最后需要一个平面列表:
f :: a -> [a]
fAppliedThrice :: [a] -> [a]
fAppliedThrice aList = concat (map f (concat (map f (concat (map f a)))))
fAppliedThrice' :: [a] -> [a]
fAppliedThrice' aList = aList >>= f >>= f >>= f
使用它的一个实际例子是我的函数获取外键关系的属性。我可以将它们链接在一起,最终获得一个平面的属性列表。例如:产品hasMany Review hasMany Tag类型关系,我最终想要一个产品的所有标签名称列表。 (我添加了一些模板-haskell并为我的目的得到了一个非常好的通用属性获取器。)
2)假设你有一系列类似过滤器的函数来应用于某些数据。他们返回了Maybe值。
case (val >>= filter >>= filter2 >>= filter3) of
Nothing -> putStrLn "Bad data"
Just x -> putStrLn "Good data"