为什么bind(>> =)存在?没有绑定的解决方案难以处理的典型情况是什么?

时间:2014-05-03 10:15:44

标签: haskell monads

这是绑定方法的类型声明:

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

我按如下方式阅读:将一个返回包装值的函数应用于包装值。

此方法作为Monad类型类的一部分包含在Prelude中。这意味着有很多情况需要它。

好的,但我不明白为什么它是典型案例的典型解决方案。

如果您已经创建了一个返回包装值的函数,为什么该函数还没有包含值?

换句话说,有许多函数采用正常值但返回包装值的典型情况是什么? (而不是获取包装值并返回包装值)

8 个答案:

答案 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给定aa->(r->b)的函数。但是你不能轻易地“展开”m a中的值,因为a没有包含在其中。 Monad是一种类型概念。


另请注意,如果您有m a->m b,则表示您没有MonadMonad为您提供了一种构建来自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]怎么样?如果我们知道我们最终将重新包装,我们可以执行“执行某些操作”三次,将xyz以及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 ()类型。与使用>>=

的desugared版本进行比较
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"