Haskell中的卫语句、if-then-else和cases语句的区别

130

我有三个函数用来找到一个列表中的第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)
在我看来,第一个函数是最佳实现,因为它最为简洁。但是,其他两个实现方案有什么特点可以使它们更优选吗?进一步地,您如何选择使用guards、if-then-else语句和cases?

5
如果您使用了case compare a 0 of LT -> ... | EQ -> ... | GT -> ...,则可以折叠嵌套的case语句。 - rampion
5
@rampion: 你的意思是 case compare a 1 of ... - newacct
4个回答

152

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

话虽如此,我的样式原则是:如果你能像阅读英语一样阅读它(将|读作“when”,将| otherwise读作“otherwise”,将=读作“is”或“be”),那么你可能做对了一些事情。

if..then..else用于当您有一个二进制条件或需要做出一个单一决策时。在Haskell中,嵌套的if..then..else表达式非常不常见,几乎总是应该使用guards代替。

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

如果在函数的顶层,每个if..then..else表达式都可以被一个guard所取代,并且通常应该这样做,因为您可以更轻松地添加更多情况:

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

case..of 用于当你有多条代码路径时,每个代码路径都由一个值的结构来引导,即通过模式匹配。你很少会匹配 TrueFalse

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

守卫可以与case..of表达式结合使用,这意味着如果你需要根据一个值做出复杂的决策,首先基于输入数据的结构做出决策,然后再基于结构中的值做决策。

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)

1
你很少会在 TrueFalse 上进行匹配,是否有任何情况需要这样做呢?毕竟,这种决策总是可以使用 if 或者 guards 来实现。 - leftaroundabout
3
例如:case (foo, bar, baz) of (True, False, False) -> ... - dflemstr
@dflemstr,难道没有更微妙的区别吗?例如,guards需要MonadPlus并返回monad实例,而if-then-else则不需要?但我不确定。 - J Fritsch
2
@JFritsch:guard函数需要MonadPlus,但我们在这里讨论的是像| test =子句中的保护条件,它们并不相关。 - Ben Millwood
谢谢你的样式提示,现在我的疑虑得到了确认。 - daparic
if ... then ... else 正是对一个 单一 Bool 进行模式匹配;它只是有专门的语法而已,而不是使用通用语法。因此,如果你要匹配一个单一的 Bool,就使用 if。但是,如果你要匹配 多个 Bool(例如 True/False 是复合模式匹配的一个组成部分),将其重写为使用嵌套模式匹配,这样你就可以用 if 替换其中一个,可能并不是一种改进。 - Ben

24

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

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

4

三种实现都能够产生正确的结果,但是GHC(截至2021年)会抱怨模式匹配不完全——这在某种程度上是真的,因为可能的模式隐藏在guards/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

最后一个模式可以匹配任何内容,因此需要在前两个模式可能成功匹配的情况下进行匹配。

4
这只是一个排序问题,但我认为它非常易读,并且具有类似于保护的结构。
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不需要if语句,因为没有其他可能性,而且函数应该有“最后的备选方案”,以防你遗漏了任何内容。

6
当你可以使用case guards时,嵌套的if语句是一种反模式。 - user76284

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