为什么不能在 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
的值为 3
,y
的值为 Just 3
。
所以我的问题是:
- 为什么语法需要
let..in
块才能正确编译?
- 在函数中,为什么我不用
let..in
的时候Just
加了Maybe
类型?
- 相反,如果它的类型是
Just :: a -> Maybe a
,为什么在函数外部使用 Just
从值中删除 Just
奖金问题,但无关:
- 当我声明没有类型的函数时,编译器会推断类型
binom :: (Ord a1, Num a2, Num a1) => a1 -> a1 -> Maybe a2
。现在我基本上明白这里发生了什么,但我不明白为什么 a1
有两种类型。
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 Nothing
和 fmap (+) x y == Nothing
如果 x
或 y
是 Nothing
。解决方案是区别对待两次不同的 n < k
。
- “初始”使用可以 return
Nothing
- “递归”使用可以简单地 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
包装器中提取基础值(这些是 x
和 y
),将它们相加并重新包装。
但是这种方法是有缺陷的。一方面,它是样板文件——如果您是第一次看到它,需要编写大量代码并尝试理解它,而底层模式非常简单:“解包值,将它们加在一起,然后重新包装”。所以应该有一种更简单、更容易理解的方法来做到这一点。还有,使用 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 a1
和 Num a1
。 Ord
和 Num
这里是类型类——就像我之前提到的 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
不是关键字,也不是控制流的一部分。
一个关键问题是,如果您有 许多 个数量可以变成 Nothing
,do 构造看起来更 可扩展,也就是说,它可以变得比模式匹配或提升函数更具可读性。在线 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)]
λ>
我有几个关于 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
的值为 3
,y
的值为 Just 3
。
所以我的问题是:
- 为什么语法需要
let..in
块才能正确编译? - 在函数中,为什么我不用
let..in
的时候Just
加了Maybe
类型? - 相反,如果它的类型是
Just :: a -> Maybe a
,为什么在函数外部使用
Just
从值中删除 Just
奖金问题,但无关:
- 当我声明没有类型的函数时,编译器会推断类型
binom :: (Ord a1, Num a2, Num a1) => a1 -> a1 -> Maybe a2
。现在我基本上明白这里发生了什么,但我不明白为什么a1
有两种类型。
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 Nothing
和 fmap (+) x y == Nothing
如果 x
或 y
是 Nothing
。解决方案是区别对待两次不同的 n < k
。
- “初始”使用可以 return
Nothing
- “递归”使用可以简单地 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
包装器中提取基础值(这些是 x
和 y
),将它们相加并重新包装。
但是这种方法是有缺陷的。一方面,它是样板文件——如果您是第一次看到它,需要编写大量代码并尝试理解它,而底层模式非常简单:“解包值,将它们加在一起,然后重新包装”。所以应该有一种更简单、更容易理解的方法来做到这一点。还有,使用 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 a1
和 Num a1
。 Ord
和 Num
这里是类型类——就像我之前提到的 Applicative 一样(尽管 Ord 和 Num 是更简单的类型类)。如果您不知道什么是类型类,我建议您阅读介绍性资源,例如 Learn You a Haskell,然后再继续使用该语言 - 但简而言之,它有点像接口,表示类型必须实现某些功能。具体来说,Ord
表示该类型必须实现顺序比较 - 你在这里需要它,因为你使用了 <
运算符 - 而 Num
表示你可以用它做数字的事情,比如加法。因此,该类型签名只是明确说明函数定义中隐含的内容 - 您使用此函数的值必须是同时实现顺序比较和数字操作的类型。
这个问题得到了很好的回答。但是,我认为值得一提的是,您还可以使用 monadic do 构造,就像通常用于 Haskell 应用程序的“主程序”一样。
主程序通常在 IO monad 中使用 do 构造。在这里,您将在 Maybe
monad.
你的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
不是关键字,也不是控制流的一部分。
一个关键问题是,如果您有 许多 个数量可以变成 Nothing
,do 构造看起来更 可扩展,也就是说,它可以变得比模式匹配或提升函数更具可读性。在线 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)]
λ>