警卫诉 if-then-else 诉哈斯克尔案件

我有三个函数可以找到列表的第 n 个元素:

nthElement :: [a] -> Int -> Maybe a
nthElement [] a = Nothing
nthElement (x:xs) a | a <= 0 = Nothing
| a == 1 = Just x
| a > 1 = nthElement xs (a-1)


nthElementIf :: [a] -> Int -> Maybe a
nthElementIf [] a = Nothing
nthElementIf (x:xs) a = if a <= 1
then if a <= 0
then Nothing
else Just x -- a == 1
else nthElementIf xs (a-1)


nthElementCases :: [a] -> Int -> Maybe a
nthElementCases [] a = Nothing
nthElementCases (x:xs) a = case a <= 0 of
True -> Nothing
False -> case a == 1 of
True -> Just x
False -> nthElementCases xs (a-1)

在我看来,第一个函数是最好的实现,因为它是最简洁的。但是,是否有其他两种实现使它们更可取呢?扩展一下,您将如何在使用警卫、 if-then-else 语句和案例之间做出选择?

83847 次浏览

从技术角度来看,这三个版本是等价的。

话虽如此,我对风格的经验法则是,如果你能把它当作英语来阅读(把 |读作“ when”,把 | otherwise读作“ other”,把 =读作“ is”或“ be”) ,那么你很可能就做对了。

if..then..else是当你有 一个二进制条件,或一个单一的决定,你需要作出。嵌套的 abc 0表达式在哈斯克尔非常少见,几乎总是应该使用警卫来代替。

let absOfN =
if n < 0 -- Single binary expression
then -n
else  n

如果每个 if..then..else表达式位于函数的顶层,那么它可以被一个约束替换,这通常是首选的,因为这样你就可以更容易地添加更多的例子:

abs n
| n < 0     = -n
| otherwise =  n

case..of是当您有 多个代码路径时使用的,并且每个代码路径都由 一个值的结构 ,即通过模式匹配。

case mapping of
Constant v -> const v
Function f -> map f

保护补充了 case..of表达式,这意味着如果需要根据值做出复杂的决策,first根据输入的结构做出决策,而 then根据结构中的值做出决策。

handle  ExitSuccess = return ()
handle (ExitFailure code)
| code < 0  = putStrLn . ("internal error " ++) . show . abs $ code
| otherwise = putStrLn . ("user error " ++)     . show       $ code

顺便说一句。作为一个风格提示,如果 =/|之后的内容对于一行来说太长,或者由于其他原因使用了更多的行,那么总是在 =之后或者 |之前换行:

-- NO!
nthElement (x:xs) a | a <= 0 = Nothing
| a == 1 = Just x
| a > 1 = nthElement xs (a-1)


-- Much more compact! Look at those spaces we didn't waste!
nthElement (x:xs) a
| a <= 0    = Nothing
| a == 1    = Just x
| otherwise = nthElement xs (a-1)

我知道这是关于显式递归函数的样式的问题,但我建议最好的样式是找到一种重用现有递归函数的方法。

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)

这只是一个顺序的问题,但我认为它非常可读,有同样的结构作为警卫。

nthElement :: [a] -> Int -> Maybe a
nthElement [] a = Nothing
nthElement (x:xs) a = if a  < 1 then Nothing else
if a == 1 then Just x
else nthElement xs (a-1)

最后一个 else 不需要,如果没有其他可能性,那么函数也应该有“最后一种情况”,以防遗漏任何内容。

尽管这三个实现都产生了正确的结果,但 GHC (截至2021年)抱怨模式匹配并不是穷举的——只要可能的模式隐藏在守卫/if/case 之后,这种抱怨就是正确的。考虑一下这个实现,它比这三个实现都更简洁,而且避免了一个非详尽的模式警告:

nthElement :: [a] -> Int -> Maybe a
nthElement (x:_) 1  = Just x
nthElement (_:xs) i = nthElement xs (i - 1)
nthElement _ _      = Nothing  -- index is out of bounds

最后一个模式匹配所有内容,因此需要低于前两个模式的可能成功匹配。