为什么你不能在Haskell中使用没有'let..in'块的'Just'语法?
我有几个关于JustHaskell 语法的问题。
当我尝试用不同的方法编写函数来计算二项式系数时出现问题。
考虑函数:
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)
当我尝试otherwise在let..in没有 let..in 块的情况下编写没有块的情况时,如下所示:
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))
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)
如果我Just在 binom 调用之前添加,错误只会复合:
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会有价值3,y也会有价值Just 3。
所以我的问题是:
- 为什么语法需要
let..in块才能正确编译? - 在函数中,为什么不使用时
Just添加Maybe类型let..in? - 相反,如果
Just函数Just的类型为Just :: a -> Maybe a
奖金问题,但不相关:
- 当我声明没有类型的函数时,编译器会推断类型
binom :: (Ord a1, Num a2, Num a1) => a1 -> a1 -> Maybe a2。现在我大致了解这里发生了什么,但我不明白为什么a1有两种类型。
回答
您的问题展示了您可能对正在发生的事情感到困惑的几种方式。
首先,Just它不是任何一种语法——它只是标准库提供的一个数据构造函数(因此也是一个函数)。因此,您的失败尝试未编译的原因不是由于任何语法错误(在这种情况下编译器会报告“解析错误”),而是 - 正如它实际报告的那样 - 类型错误。换句话说,编译器能够解析代码以理解它,但是在检查类型时,意识到有些事情发生了。
因此,为了扩展您失败的尝试,#1 是这样的:
报告的错误是
No instance for (Num (Maybe Integer)) arising from a use of ‘+’
这是因为您试图将 2 次调用的结果添加到binom- 根据您的类型声明,是 type 的值Maybe Integer。而且 Haskell 默认不知道如何添加两个Maybe Integer值(会Just 2 + Nothing是什么?),所以这不起作用。您需要 - 正如您最终成功尝试所做的那样 - 解开底层的 Integer 值(假设它们存在!我稍后会回到这个问题),将它们相加,然后将结果总和包装在Just.
我不会详述其他失败的尝试,但希望您能看到,在各种方面,这些类型也无法以编译器描述的方式在这里匹配。在 Haskell 中,您真的必须了解类型,而只是随意地抛开各种语法和函数调用,希望最终能够编译,这会导致挫折和失败!
所以对于你明确的问题:
为什么语法需要 let..in 块才能正确编译?
它没有。它只需要在任何地方匹配的类型。你最终得到的版本:
let
Just x = (binom (n-1) (k-1))
Just y = (binom (n-1) k)
in
Just (x + y)
很好(从类型检查的角度来看,无论如何!)因为您正在按照我之前描述的方式进行 - 从Just包装器中提取基础值(这些是x和y),将它们相加并重新包装它们。
但这种方法是有缺陷的。一方面,它是样板文件 - 如果您是第一次看到它,需要编写大量代码并尝试理解它,而底层模式非常简单:“解开值,将它们加在一起,然后重新包装”。所以应该有一种更简单、更容易理解的方法来做到这一点。还有,使用 Applicative 类型类的方法 - 该Maybe类型是其中的成员。
有经验的 Haskellers 会以两种方式之一编写上述内容。任何一个:
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
(在所谓的“应用性风格”后者-如果你不熟悉应用型函子有一个在学习你Haskell的一个伟大的介绍在这里。)
与您的方式相比,这样做还有另一个优势,除了避免样板代码。let... in表达式中的模式匹配假定等的结果binom (n-1) (k-1)的形式为Just x。但它们也可能是Nothing- 在这种情况下,您的程序将在运行时崩溃!正如@chepner 在他的回答中所描述的那样,这确实会发生在您的情况下。
由于 Applicative 实例的实现方式,使用liftA2或<*>将Maybe避免崩溃,只要Nothing您尝试添加的内容之一是 Nothing 就可以避免崩溃。(这反过来意味着您的函数将始终返回Nothing- 我会让您自行找出解决方法!)
我不确定我是否真的理解你的问题 #2 和 #3,所以我不会直接解决这些问题 - 但我希望这能让你对如何Maybe在 Haskell 中工作有更多的了解。最后,对于您的最后一个问题,尽管它很不相关:“我不明白为什么 a1 有两种类型”-它没有。a1表示单一类型,因为它是单一类型变量。您大概指的是它有两个约束- hereOrd a1和Num a1. Ord和Num这里是类型类 - 就像我之前提到的 Applicative 一样(尽管 Ord 和 Num 是更简单的类型类)。如果您不知道什么是类型类,我建议您先阅读介绍性资源,例如 Learn You a Haskell,然后再继续使用该语言 - 但简而言之,它有点像一个接口,说明类型必须实现某些功能。具体来说,Ord说类型必须实现顺序比较——你在这里需要它,因为你已经使用了<运算符——而Num说你可以用它做数字事情,比如加法。因此,该类型签名只是明确了您的函数定义中隐含的内容 - 您使用此函数的值必须是同时实现顺序比较和数字运算的类型。