为什么不能在 Haskell 中使用没有 'let..in' 块的 'Just' 语法?

Why can't you use 'Just' syntax without 'let..in' block in Haskell?

我有几个关于 Haskell 中的 Just 语法的问题。

当我尝试用不同的方法编写函数来计算 binomial coefficients 时出现问题。

考虑函数:

binom :: Integer -> Integer -> Maybe Integer
binom n k | n < k     = Nothing
binom n k | k == 0    = Just 1
binom n k | n == k    = Just 1
binom n k | otherwise = let 
                          Just x = (binom (n-1) (k-1))
                          Just y = (binom (n-1) k)
                        in
                          Just (x + y)

当我尝试在没有 let..in 块的情况下编写没有 let..in 块的 otherwise 案例时:

binom n k | otherwise = (binom (n-1) (k-1)) + (binom (n-1) k)

我遇到编译错误 No instance for (Num (Maybe Integer)) arising from a use of ‘+’。所以我的第一个想法是我忘记了 Just 语法所以我将它重写为

binom n k | otherwise = Just ((binom (n-1) (k-1)) + (binom (n-1) k))

我遇到了一个更令人困惑的错误:

Couldn't match type ‘Maybe Integer’ with ‘Integer’
      Expected: Maybe Integer
        Actual: Maybe (Maybe Integer)

如果我在 binom 调用之前添加 Just,错误只会复合:

Couldn't match type ‘Maybe (Maybe Integer)’ with ‘Integer’
      Expected: Maybe Integer
        Actual: Maybe (Maybe (Maybe Integer))

此外,如果我写:

Just x = binom 3 2
y = binom 3 2

x 的值为 3y 的值为 Just 3

所以我的问题是:

  1. 为什么语法需要 let..in 块才能正确编译?
  2. 在函数中,为什么我不用let..in的时候Just加了Maybe类型?
  3. 相反,如果它的类型是 Just :: a -> Maybe a
  4. ,为什么在函数外部使用 Just 从值中删除 Just

奖金问题,但无关:

binom n k | otherwise = (binom (n-1) (k-1)) + (binom (n-1) k)

您不能添加两个 Maybe 值,但您可以使用 Functor 实例添加已包含在 Just.

中的值
binom n k | otherwise = fmap (+) (binom (n-1) (k-1)) (binom (n-1) k)

这不太有效,因为最终递归调用将 return Nothingfmap (+) x y == Nothing 如果 xyNothing。解决方案是区别对待两次不同的 n < k

  1. “初始”使用可以 return Nothing
  2. “递归”使用可以简单地 return 0,因为 x + 0 == x.

binom 将根据保证接收参数的助手来实现 n >= k.

binom :: Integer -> Integer -> Maybe Integer
binom n k | n < k     = Nothing
          | otherwise = Just (binom' n k)
  where binom' n 0 = 1
        binom' n k | n == k = 1
                   | otherwise = binom' (n-1) (k-1) + binom' (n-1) k

您的问题展示了您可能对正在发生的事情感到困惑的几种方式。

首先,Just 不是任何一种语法——它只是标准库提供的数据构造函数(因此也是一个函数)。因此,您失败的尝试未编译的原因不是由于任何语法错误(在这种情况下编译器会报告“解析错误”),而是 - 正如它实际报告的那样 - 类型错误。换句话说,编译器能够解析代码以理解它,但随后在检查类型时,意识到有问题。

因此,为了进一步说明您的失败尝试,#1 是这样的:

binom n k | otherwise = Just ((binom (n-1) (k-1)) + (binom (n-1) k))

报告的错误是

No instance for (Num (Maybe Integer)) arising from a use of ‘+’

这是因为您试图将 2 次调用的结果添加到 binom - 根据您的类型声明,它们是 Maybe Integer 类型的值。并且 Haskell 默认情况下不知道如何添加两个 Maybe Integer 值(Just 2 + Nothing 会是什么?),所以这是行不通的。您将需要 - 正如您最终成功尝试所做的那样 - 展开底层 Integer 值(假设它们存在!我稍后会回来讨论),将它们相加,然后将结果总和包装在 Just.

我不会详述其他失败的尝试,但希望您能看到,以各种方式,这里的类型也无法按照编译器描述的方式匹配。在 Haskell 中,您确实必须了解类型,只是胡乱乱扔各种语法和函数调用,希望最终能够编译,这只会导致挫败感和失败!

所以对于你明确的问题:

Why does the syntax requite the let..in block to compile properly?

没有。它只需要类型在任何地方都匹配。您最终得到的版本:

let 
  Just x = (binom (n-1) (k-1))
  Just y = (binom (n-1) k)
in
  Just (x + y)

很好(从类型检查的角度来看,无论如何!)因为你正在按照我之前描述的那样做 - 从 Just 包装器中提取基础值(这些是 xy),将它们相加并重新包装。

但是这种方法是有缺陷的。一方面,它是样板文件——如果您是第一次看到它,需要编写大量代码并尝试理解它,而底层模式非常简单:“解包值,将它们加在一起,然后重新包装”。所以应该有一种更简单、更容易理解的方法来做到这一点。还有,使用 Applicative 类型类的方法 - Maybe 类型是其中的一个成员。

有经验的 Haskell 人员会以两种方式之一编写上述内容。或者:

binom n k | otherwise = liftA2 (+) (binom (n-1) (k-1)) (binom (n-1) k)

binom n k | otherwise = (+) <$> binom (n-1) (k-1) <*> binom (n-1) k

(后者属于所谓的“应用风格”——如果您不熟悉应用函子,请参阅 Learn You a Haskell here。)

除了避免样板代码之外,与您的方式相比,这样做还有另一个优势。您的模式匹配 let... in 表达式 假设 binom (n-1) (k-1) 等的结果是 Just x 的形式。但它们也可能是 Nothing - 在这种情况下,您的程序将在运行时崩溃!正如@chepner 在他的回答中所描述的那样,这确实会发生在你的情况下。

使用 liftA2<*>,由于 Maybe 的 Applicative 实例是如何实现的,只要尽快给你 Nothing 就可以避免崩溃您尝试添加的内容中没有任何内容。 (这反过来意味着您的功能将始终 return Nothing - 我会留给您解决它的问题!)

我不确定我是否真的理解您的问题 #2 和 #3,所以我不会直接解决这些问题 - 但我希望这能让您对如何使用 Maybe 有更多的了解在 Haskell。最后是你的最后一个问题,虽然它是完全无关的:“我不明白为什么 a1 有两种类型” - 它没有。 a1表示单一类型,因为它是单一类型变量。您可能指的是它有两个 约束 - 这里是 Ord a1Num a1OrdNum 这里是类型类——就像我之前提到的 Applicative 一样(尽管 Ord 和 Num 是更简单的类型类)。如果您不知道什么是类型类,我建议您阅读介绍性资源,例如 Learn You a Haskell,然后再继续使用该语言 - 但简而言之,它有点像接口,表示类型必须实现某些功能。具体来说,Ord 表示该类型必须实现顺序比较 - 你在这里需要它,因为你使用了 < 运算符 - 而 Num 表示你可以用它做数字的事情,比如加法。因此,该类型签名只是明确说明函数定义中隐含的内容 - 您使用此函数的值必须是同时实现顺序比较和数字操作的类型。

这个问题得到了很好的回答。但是,我认为值得一提的是,您还可以使用 monadic do 构造,就像通常用于 Haskell 应用程序的“主程序”一样。

主程序通常在 IO monad 中使用 do 构造。在这里,您将在 Maybe monad.

中使用 do 构造

你的binom函数可以这样修改:

binom :: Integer -> Integer -> Maybe Integer
binom n k | n < 0     = Nothing  -- added for completeness
binom n k | k < 0     = Nothing  -- added for completeness
binom n k | n < k     = Nothing
binom n k | k == 0    = Just 1
binom n k | n == k    = Just 1
binom n k | otherwise = do  -- monadic do construct, within the Maybe monad
                            x <- (binom (n-1) (k-1))
                            y <- (binom (n-1) k)
                            return (x+y)

main :: IO ()
main = do  -- classic monadic do construct, within the IO monad
    putStrLn "Hello impure world !"
    putStrLn $ show (binom 6 3)

如果单个 <- 提取器失败,则整个结果为 Nothing

请记住,在那种情况下,return 只是一个普通函数,类型签名为:

return :: Monad m => a -> m a

与大多数命令式语言不同,return 不是关键字,也不是控制流的一部分。

一个关键问题是,如果您有 许多 个数量可以变成 Nothingdo 构造看起来更 可扩展,也就是说,它可以变得比模式匹配或提升函数更具可读性。在线 Real World Haskell 书中有关使用 Maybe monad 的更多详细信息。

请注意,Haskell 库不仅提供 liftA2, as mentioned in Robin Zigmond's answer, but also other lift'ing functions up to lift6

交互式测试:

你可以在 ghci 解释器下测试这个东西,像这样:

$ ghci
 GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
 λ> 
 λ> do { n1 <- (Just 3) ; n2 <- (Just 42); return (n1+n2) ; }
 Just 45
 λ> 
 λ> do { n1 <- (Just 3) ; n2 <- (Just 42); n3 <- Nothing ; return (n1+n2+n3) ; }
 Nothing
 λ> 

确切的语义取决于所涉及的 monad 的种类。如果你使用列表 monad,你会得到你从中提取的列表的笛卡尔积:

 λ> 
 λ> do { n1 <- [1,2,3] ; n2 <- [7,8,9];  return (n1,n2) ; }
 [(1,7),(1,8),(1,9),(2,7),(2,8),(2,9),(3,7),(3,8),(3,9)]
 λ>