在Haskell中使用保护比使用递归函数的模式更好吗?

时间:2011-05-20 20:18:09

标签: layout haskell recursion pattern-matching guard

我只是想知道我在Haskell中设置的递归函数。使用保护通常比递归函数的模式更好吗?

我只是不确定最佳布局是什么,但我知道在定义这样的函数时模式更好:

units :: Int -> String

units 0 = "zero"
units 1 = "one"

更受欢迎
units n
    | n == 0 = "zero"
    | n == 1 = "one"

我只是不确定递归是否相同或不同。

对术语不太确定:我使用的是这样的东西:

f y [] = [] 
f y (x:xs) 
    | y == 0 = ...... 
    | otherwise = ...... 

还是会更好?

f y [] = [] 
f 0 (x:xs) = 
f y (x:xs) =

5 个答案:

答案 0 :(得分:9)

我的一般经验法则是:

  • 使用模式匹配时,后卫将是一个简单的==检查。

通过递归,您通常会检查基本情况。因此,如果您的基本案例是简单的==检查,那么请使用模式匹配。

所以我通常会这样做:

map f [] = []
map f (x:xs) = f x : map f xs

而不是这个(null只检查列表是否为空。它基本上是== []):

map f xs | null xs   = []
         | otherwise = f (head xs) : map f (tail xs)

模式匹配是为了让你的生活更轻松,imho,所以最后你应该做对你有意义的事情。如果您与团队合作,那么请对团队做有意义的事情。

[更新]

对于你的具体情况,我会做这样的事情:

f _ []      = []
f 0 _       = ...
f y (x:xs)  = ...

模式匹配,如警卫,从上到下,在与输入匹配的第一个定义处停止。我使用下划线符号表示对于第一个模式匹配,我不关心y参数是什么,对于第二个模式匹配,我不关心列表参数是什么(尽管,如果你确实使用该计算中的列表,那么你不应该使用下划线)。因为它仍然相当简单== - 就像检查一样,我个人坚持使用模式匹配。

但我认为这是个人偏好的问题;你的代码是完全可读和正确的。如果我没有弄错的话,在编译代码时,守护和模式匹配最终都会变成case语句。

答案 1 :(得分:4)

一个简单的规则

  • 如果要对数据结构进行递归,请使用模式匹配
  • 如果您的递归条件更复杂,请使用警卫。

讨论

从根本上说,它取决于您希望做的测试以防止递归。如果它是对数据类型结构的测试,请使用模式匹配,因为它比冗余测试更有效。

对于您的示例,整数上的模式匹配显然更清晰,更有效:

units 0 = "zero"
units 1 = "one"

对于任何数据类型的递归调用都是如此,您可以通过数据的形状区分案例。

现在,如果你有更复杂的逻辑条件,那么警卫就会有意义。

答案 2 :(得分:2)

对此没有真正严格的规则,这就是为什么你得到的答案有点朦胧。有些决定很简单,比如[]上的模式匹配,而不是用f xs | null xs = ...守护,或者天堂禁止,f xs | length xs == 0 = ...这在多种方面都是可怕的。但是,当没有令人信服的实际问题时,只需使用哪个使代码更清晰。

作为一个例子,考虑这些功能(实际上并没有做任何有用的事情,只是作为插图):

f1 _ [] = [] 
f1 0 (x:xs) = [[x], xs]
f1 y (x:xs) = [x] : f1 (y - 1) xs

f2 _ [] = []
f2 y (x:xs) | y == 0    = calc 1 : f2 (- x) xs
            | otherwise = calc (1 / y) : f2 (y * x) xs
  where calc z = x * ...

f1中,单独的模式强调递归有两个基本情况。在f2中,警卫强调0对于某些计算来说只是一个特殊情况(大部分由calc完成,在守卫的两个分支共享的where子句中定义)并且不会改变计算的结构。

答案 3 :(得分:2)

@Dan是正确的:它基本上是个人偏好的问题,不会影响生成的代码。这个模块:

module Test where

units :: Int -> String
units 0 = "zero"
units 1 = "one"

unitGuarded :: Int -> String
unitGuarded n
  | n == 0 = "zero"
  | n == 1 = "one"

产生了以下核心:

Test.units =
  \ (ds_dkU :: GHC.Types.Int) ->
    case ds_dkU of _ { GHC.Types.I# ds1_dkV ->
    case ds1_dkV of _ {
      __DEFAULT -> Test.units3;
      0 -> Test.unitGuarded2;
      1 -> Test.unitGuarded1
    }
    }

Test.unitGuarded =
  \ (n_abw :: GHC.Types.Int) ->
    case n_abw of _ { GHC.Types.I# x_ald ->
    case x_ald of _ {
      __DEFAULT -> Test.unitGuarded3;
      0 -> Test.unitGuarded2;
      1 -> Test.unitGuarded1
    }
    }

完全相同,除了不同的默认情况,在两个实例中都是模式匹配错误。 GHC甚至为匹配的案例共同编写了字符串。

答案 4 :(得分:2)

到目前为止,答案中没有提到模式匹配的优势,这对我来说是最重要的:安全地实现全部功能的能力。

进行模式匹配时,您可以安全地访问对象的内部结构,而不必担心此对象是其他内容。如果您忘记了某些模式,编译器可以向您发出警告(遗憾的是,此警告在GHC中默认为关闭)。

例如,写这篇文章时:

map f xs | null xs   = []
         | otherwise = f (head xs) : map f (tail xs)

您被迫使用非全部函数headtail,从而冒着程序生命的风险。如果你在保护条件中犯了错误,编译器就无法帮助你。

另一方面,如果使用模式匹配发生错误,编译器可能会根据错误的严重程度给出错误或警告。

一些例子:

-- compiles, crashes in runtime
map f xs | not (null xs)   = []
         | otherwise = f (head xs) : map f (tail xs)

-- does not have any way to compile
map f (h:t) = []
map f [] = f h : map f t


-- does not give any warnings
map f xs = f (head xs) : map f (tail xs)

-- can give a warning of non-exhaustive pattern match
map f (h:t) = f h : map f t