Haskell中的Guards vs. if-then-else vs. case

Posted

技术标签:

【中文标题】Haskell中的Guards vs. if-then-else vs. case【英文标题】:Guards vs. if-then-else vs. cases in Haskell 【发布时间】:2012-03-09 21:01:52 【问题描述】:

我有三个函数可以找到列表的第 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 语句和案例之间进行选择?

【问题讨论】:

如果您使用了case compare a 0 of LT -&gt; ... | EQ -&gt; ... | GT -&gt; ... ,您可以折叠嵌套的case 语句 @rampion:你的意思是case compare a 1 of ... 【参考方案1】:

虽然所有三种实现都产生了正确的结果,但 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

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

【讨论】:

【参考方案2】:

这只是一个排序问题,但我认为它非常易读,并且具有与守卫相同的结构。

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 语句是一种反模式。【参考方案3】:

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

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

【讨论】:

【参考方案4】:

从技术角度来看,所有三个版本都是等效的。

话虽如此,我对样式的经验法则是,如果您可以像阅读英语一样阅读它(将| 阅读为“when”,| otherwise 阅读为“otherwise”,= 阅读为“is”或“be”),你可能做对了。

if..then..else 适用于当您有一个二元条件,或者您需要做出一个单一决定时。嵌套的if..then..else-表达式在 Haskell 中非常少见,几乎总是应该使用守卫。

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

如果每个if..then..else 表达式位于函数的顶层,则可以将其替换为守卫,这通常应该是首选,因为您可以更轻松地添加更多案例:

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

case..of 适用于当您有多个代码路径,并且每个代码路径都由 值的结构,即通过模式匹配。你很少匹配TrueFalse

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

Guards 补充了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)

【讨论】:

“你很少匹配TrueFalse”有什么场合你会这样做吗?毕竟,这种决定总是可以通过if 和守卫来完成。 例如case (foo, bar, baz) of (True, False, False) -&gt; ... @dflemstr 没有更细微的区别吗?守卫需要 MonadPlus 并返回一个 monad 实例,而 if-then-else 不需要?但我不确定。 @JFritsch:guard 函数需要MonadPlus,但我们这里讨论的是| test = 子句中的守卫,它们不相关。 感谢您的风格提示,现在它已被确认。

以上是关于Haskell中的Guards vs. if-then-else vs. case的主要内容,如果未能解决你的问题,请参考以下文章

与项目欧拉速度比较:C vs Python与Erlang vs Haskell

2015 NCPC Problem G-Goblin Garden Guards

使用 Guards (Roles, JWT) 获取用户数据

[CareerCup] Guards in a museum 博物馆的警卫

GHC ccall 安全 VS ccall 不安全

使用 Laravel Guards 登录后,如何限制用户不访问登录页面?