11

我只是想知道我在 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) =
4

5 回答 5

10

我的一般经验法则是:

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

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

所以我通常会这样做:

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)

模式匹配是为了让你的生活更轻松,恕我直言,所以最后你应该做对你有意义的事情。如果你和一个团队一起工作,那么做对这个团队有意义的事情。

[更新]

对于你的特殊情况,我会做这样的事情:

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

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

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

于 2011-05-20T20:36:05.930 回答
4

一个简单的规则

  • 如果您在数据结构上递归,请使用模式匹配
  • 如果您的递归条件更复杂,请使用守卫。

讨论

从根本上说,这取决于您希望为保护递归而进行的测试。如果它是对数据类型结构的测试,请使用模式匹配,因为它比对相等性的冗余测试更有效。

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

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

对任何数据类型的递归调用也是如此,您可以通过数据的形状来区分大小写。

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

于 2011-05-20T20:21:01.780 回答
2

在这方面并没有真正的硬性规定,这就是为什么你得到的答案有点模糊。有些决定很容易,比如模式匹配[]而不是用f xs | null xs = ...or 来保护,天堂禁止,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)并且不会改变计算的结构。

于 2011-05-20T21:19:23.660 回答
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 甚至统一了匹配案例的字符串。

于 2011-05-20T23:07:06.833 回答
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
于 2011-05-21T05:45:41.610 回答