向Monad传递多个参数

5

我正在学习 Haskell 并编写一些示例。不确定为什么第二个示例无法工作。

foo :: Int -> Int -> Maybe Int
foo 0 0 = Nothing
foo a b = Just $ a + b

bar :: Int -> Maybe Int
bar 0 = Nothing
bar a = Just $ a + 1

-- This works
Just 4 >>= bar

-- Why this doesn't work?
(Just 4 Just 4) >>= foo

-- This works
do
    a <- Just 3
    b <- Just 4
    foo a b

5
(Just 4 Just 4) 调用了 Just 并传入了三个参数,但是 Just 只接收一个参数。同时请注意,monad 的作用在于强制你对顺序要求严格:如果你有两个操作,你需要使用两个 >>=(或者在 do 中占据两行)。例如 Just 3 >>= \x -> Just 4 >>= \y -> foo x y - chi
3个回答

6
正如注释所说,(Just 4 Just 4)试图将构造函数Just应用于3个参数,而它只需要一个参数。因此,我假设你想要像(Just 4, Just 4)一样的东西,并希望它像你的最终例子一样工作。
“bind”运算符的类型为(>>=) :: Monad m => m a -> (m a -> b) -> m b。这意味着操作符之后预期的函数只接受一个参数,而不是两个。因此,再次说明它不能正常工作的最终原因是,你的函数使用了错误数量的参数。(部分应用意味着你不必一次性提供所有参数,但听起来你期望某些其他数据被神奇地路由到缺少的参数中……)
将您的do示例解糖为>>=形式相当于:
Just 3 >>= \a -> Just 4 >>= \b -> foo a b

为了使这更加清晰,我将括号化λ表达式:
Just 3 >>= ( \a -> Just 4 >>= (\b -> foo a b) )

这使得我们更容易看出可以简化内部lambda:

Just 3 >>= ( \a -> Just 4 >>= foo a )

所以,将缺失的数据路由到额外的参数中是可能的!但是,你必须自己解决路由问题...

Haskell函数并没有什么特别神奇的地方;它们往往比动态语言更注重被调用的方式。最大的“魔法”在于类型检查器通常可以告诉你何时使用它们不正确。

而且(如其他答案所述),>>=并没有什么神奇之处——它只是另一个函数,并且为了理解如何使用它,您需要查看其类型。


4
它不起作用是因为>>=是一个完全正常的运算符(操作符是完全正常的函数)。
你似乎认为>>=是从其左侧的单子值中获取值并将其馈送到右侧函数的特殊语法。它不是特殊语法;相反,>>=本身就是一个函数,应用于其左侧和右侧的值(然后像你期望的那样计算结果)。
然而,这意味着左侧和右侧参数必须是有效表达式,可以存在为普通值的事物;可以使用var = <expr>语法将其简单绑定到变量的东西。 Just 4 >>= bar之所以起作用,是因为(除其他要求外)Just 4本身是类型为Maybe Int的正确表达式,而bar是类型为Int -> Maybe Int的正确表达式。 Just 4 Just 4 >>= foo不起作用,因为Just 4 Just 4不是正确的表达式(它的类型将是什么?);它被解释为将Just应用于3个单独的参数4Just4,而你想要的是两个单独的值Just 4Just 4。但即使您可以让编译器将某些内容解释为两个单独的值,也没有办法将两个单独的值作为其左侧参数传递给>>=;在这种用法中,它期望类型为Just Int的单个值。
如果您有一个像foo这样需要两个参数的函数,并且您希望从处于单子上下文中的值中提取这些参数,则不能仅应用>>=,您需要编写执行此操作的代码(例如,使用do块的最后一个示例;还有许多其他方式实现类似的功能)。

2
其他答案已经解释了为什么这样做行不通。但我认为你想要这个很合理,的确Just 3 >>= \x -> Just 4 >>= \y -> foo x y对于这个任务来说有点傻。基本上,xy的值彼此独立,尽管你按顺序获取它们,但完整的y计算原则上可能取决于x的值。

单子在这里并不是正确的抽象,它们太强了。为了获得非顺序的xy,可以使用Applicative接口。目前大多数Haskeller最喜欢的形式(我认为)是:

   foo <$> Just 3 <*> Just 4

你可以这样理解:“将有副作用的值Just 3Just 4压缩成一个包含两个值的单一操作,然后对这些值应用foo。”实际上,它并不是这样工作的。对我来说,在刚开始学习应用程序时,这非常令人困惑。换句话说,上述表达式实际上被解析为:
   (foo <$> Just 3) <*> Just 4

看起来又像是顺序风格。但实际上不是,这里发生的只是一种柯里化/惰性技巧,可以将多个值通过应用值传递,而无需将它们分组到合适的元组中。代码字面上的工作方式就像我解释的那样:

  uncurry foo <$> ((,)<$>Just 3<*>Just 4)

在这里,(,)<$>Just 3<*>Just 4 的计算结果是 Just (3,4)。然后需要以uncurried形式对foo进行fmapping,因此将两个参数作为一个元组接受。结构上很清楚,但因为我们正在与Haskell的柯里化风格作斗争,所以有些笨拙。
(数学上,这种元组化实际上是概念上发生的事情:一般来说,您正在使用一个monoidal category。一些其他应用函子的具体实现有这样一个元组组合器作为它们的底层接口,而不是<*>;例如,>*<来自invertible。)
对于foo<$>Just 3<*>Just 4,技巧在于,我们不是构建一个元组,而是从部分应用的foo开始,将其应用于3的结果中。这实际上并不需要任何applicative / monadic - 我们基本上只是从3foo 3纯粹地转换包含值的(通常是多个值的)- 一般而言 - 而不影响它们的上下文。您可以将其视为纯符号操作。请注意,此时类型为Maybe (Int -> Int)
然后使用<*>组合器将两个Maybe上下文一起压缩,并同时将foo 3部分求值函数应用于其第二个参数。
我个人喜欢这种等同的形式:
  liftA2 foo (Just 3) (Just 4)

我们还没有完成:所有上述建议都给出了类型为Maybe (Maybe Int)的结果。要将其扁平化为Maybe Int,您实际上需要使用单子界面。其中一种选择是:
   join $ foo <$> Just 3 <*> Just 4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接