为什么在整数除法中使用应用函子比单子差?

5

我正在阅读 Graham Hutton 的《Haskell 编程》一书,以下展示的思路流程让我感到困惑。

他使用下面的例子来激励使用单子,并展示应用函子在除法操作中的不足,其中返回类型是 Maybe,以处理可能出现的 除以零 错误情况。

给定:

data Expr = Val Int | Div Expr Expr

safediv :: Int -> Int -> Maybe Int
safediv _ 0 = Nothing
safediv n m = Just (n `div` m)

eval :: Expr -> Maybe Int
eval (Val n) = pure n                               --type: Just(n)?
eval (Div x y) = pure safediv <*> eval x <*> eval y --type: Maybe(Maybe Int)?

他接着解释道:
然而,这个定义并不是类型正确的。特别地,函数safediv的类型为Int->Int->Maybe Int,在这种情况下需要一个类型为Int->Int->Int的函数。
即使用自定义定义的函数代替pure safediv也没有帮助,因为这个函数需要有类型Maybe(Int->Int->Int),它没有提供任何方式来指示第二个整数参数为零时的失败。(X)
结论是函数eval不符合适用函子捕捉的有副作用编程模式。适用函子的样式限制我们将纯函数应用于有副作用的参数:eval不符合这种模式,因为用于处理结果值的函数safediv不是纯函数,但本身可能失败。
我不是Haskell程序员,但从eval(Div x y)的类型中看来,它似乎是Maybe(Maybe Int) - 这可以简单地被“压缩”,是吗?(类似于Scala中的flatten或Haskell中的join)。这里真正的问题是什么?
无论x,y是否为Just(s)/Nothing(s),safediv似乎都能正确计算 - 这里唯一的问题是可以适当转换的返回类型。作者如何从他的论点得出这个结论是我难以理解的问题。
适用函子的样式限制我们将纯函数应用于有副作用的参数。
还有,为什么上面标记为(X)的段落会做出这样的声明,当问题似乎只是返回类型不匹配。
我了解适用函子可以用于更有效地链接计算,其中一个的结果不会影响另一个 - 但在这种情况下,我实在不明白失败会发生在哪里,如果只是简单的返回类型修复就能解决问题:
eval (Div x y) = join(pure safediv <*> eval x <*> eval y)

而且 safediv 必须是纯的吗?据我所知,它也可以是类型 F[Maybe]F[Either],不是吗?我可能错过了什么吗?我能看到他的思路,但我不确定这是否是达到目的的正确示例。


4
应用函子(Applicatives)没有“join”概念。要使用“join”,您需要一个单子(Monad)。这就是整个重点:应用函子不够,您需要一个单子。 - Fyodor Soikin
换句话说,当你的数据类型具有join(除了来自Applicative Functor的其他内容之外),它就是一个Monad。 - Will Ness
1个回答

10
我不是Haskell程序员,但从eval (Div x y)的类型来看,它似乎是Maybe(Maybe Int)类型的——这可以简单地被“压缩”,对吗?(类似于Scala中的flatten或Haskell中的join)。真正的问题是什么?…唯一的问题在于返回类型,它可以适当地进行转换。
这确实是关键问题!“压缩”是一种基本的单子操作——事实上,join的类型签名为join :: Monad m => m (m a) -> m a。如果你只使用纯的应用方法pure(<*>),就没有办法实现这个操作,但是如果你允许自己使用(>>=),那么这个问题变得很容易解决。当然,你可以轻松地实现flattenMaybe :: Maybe (Maybe a)) -> Maybe a而不使用单子,但这违背了像ApplicativeMonad这样的概念的目的,这些概念应该适用于广泛的类型,而不仅仅是Maybe
无论x,yJust(s)/Nothing(s)safediv似乎都会正确地计算——唯一的问题在于返回类型,可以适当地进行转换。作者是如何从他的论点得出这个结论的,我很难理解。
这里的想法是这样的。假设你有两个函数和两个值:
nonEffectful :: a -> b -> c
effectful    :: a -> b -> m c

effectfulA :: m a
effectfulB :: m b
现在,如果你想将nonEffectful函数应用于两个有副作用的参数,m只需要是Applicative:很容易使用nonEffectful <$> effectfulA <*> effectfulB :: m c。但是,如果您尝试使用effectful函数而不是它,您会遇到一个问题:返回类型变成了m (m c)而不是m c。为了将m (m c)“压缩”成m c,你需要一个Monad实例。因此,applicative只能将纯函数(非有副作用)应用于有副作用的参数,但monad允许我们将有副作用的函数应用于有副作用的参数。这就是Hutton试图做的事情,但使用了特定的函数safeDiv::Int->Int->Maybe Int
(上面讨论中没有提到的一件事是直觉:在直觉层面上,为什么需要monad进行特定的计算?正如你已经注意到的,答案与依赖性有关。对于nonEffectful <$> effectfulA <*> effectfulB,两个有副作用的值互相没有影响。然而,对于effectful <$> effectfulA <*> effectfulB,突然之间有一个依赖关系:effectful函数必须依赖于传递给它的有副作用计算结果。Monad可以被认为代表了具有相互依赖的有副作用计算的概念,而Applicative代表了不能相互依赖的有副作用计算的概念(尽管纯函数可能会依赖它们)。同样,在评估嵌套计算m(m a)时,您首先需要评估外部计算,然后评估得到的内部有副作用计算。再次出现了一个有副作用的计算依赖于另一个有副作用的计算,因此这就需要一个Monad。)

啊!我明白了。那很有道理。谢谢。 - PhD

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