在 Haskell 中,对于递归函数使用守卫比模式更好吗?

发布于 2024-11-09 03:15:33 字数 514 浏览 3 评论 0原文

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

I'm just wondering about a recursion function I'm laying out in Haskell. Is it generally better to use guards than patterns for recursion functions?

I'm just not sure on what the best layout is but I do know that patterns are better when defining functions such as this:

units :: Int -> String

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

is much preferred to

units n
    | n == 0 = "zero"
    | n == 1 = "one"

I'm just not sure though when it comes to recursion as to whether this is the same or different.

Just not quite sure on terminology: I'm using something like this:

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

or would this be better?

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

很糊涂小朋友 2024-11-16 03:15:34

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

在进行模式匹配时,您可以安全地访问对象的内部结构,而不必担心该对象是其他对象。如果您忘记了某些模式,编译器会向您发出警告(不幸的是,该警告在 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

The answers so far do not mention the advantage of pattern matching which is the most important for me: ability to safely implement total functions.

When doing pattern matching you can safely access the internal structure of the object without the fear of this object being something else. In case you forget some of the patterns, the compiler can warn you (unfortunately this warning is off by default in GHC).

For example, when writing this:

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

You are forced to use non-total functions head and tail, thus risking the life of your program. If you make a mistake in guard conditions, the compiler can't help you.

On the other hand, if you make an error with pattern matching, the compiler can give you an error or a warning depending on how bad your error was.

Some examples:

-- 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
情绪操控生活 2024-11-16 03:15:33

我的一般经验法则是:

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

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

所以我通常会这样做:

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 语句。

My general rule of thumb would be this:

  • Use pattern matching when the guard would be a simple == check.

With recursion, you usually are checking for a base case. So if your base case is a simple == check, then use pattern matching.

So I'd generally do this:

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

Instead of this (null simply checks if a list is empty. It's basically == []):

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

Pattern matching is meant to make your life easier, imho, so in the end you should do what makes sense to you. If you work with a group, then do what makes sense to the group.

[update]

For your particular case, I'd do something like this:

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

Pattern matches, like guards, fall from top to bottom, stopping at the first definition that matches the input. I used the underscore symbol to indicate that for the first pattern match, I didn't care what the y argument was, and for the second pattern match, I didn't care what the list argument was (although, if you do use the list in that computation, then you should not use the underscore). Since it's still fairly simple ==-like checks, I'd personally stick with pattern matching.

But I think it's a matter of personal preference; your code is perfectly readable and correct as it is. If I'm not mistaken, when the code is compiled, both guards and pattern matches get turned into case statements in the end.

苍暮颜 2024-11-16 03:15:33

一个简单的规则

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

讨论

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

对于您的示例,整数上的模式匹配显然更干净、更高效:

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

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

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

A simple rule

  • If you are recursing on a data structure, use pattern matching
  • If your recursive condition is more complex, use guards.

Discussion

Fundamentally, it depends on the test you wish to do to guard the recursion. If it is a test on the structure of a data type, use pattern matching, as it will be more efficient than redundant testing for equality.

For your example, pattern matching on the integers is obviously cleaner and more efficient:

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

The same goes for recursive calls on any data type, where you distinguish cases via the shape of the data.

Now, if you had more complicated logical conditions, then guards would make sense.

做个ˇ局外人 2024-11-16 03:15:33

这方面并没有真正硬性规定,这就是为什么你得到的答案有点模糊。有些决定很简单,例如在 [] 上进行模式匹配,而不是使用 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中定义)子句由守卫的两个分支共享)并且不会改变计算的结构。

There aren't really hard and fast rules on this, which is why the answers you've gotten were a bit hazy. Some decisions are easy, like pattern matching on [] instead of guarding with f xs | null xs = ... or, heaven forbid, f xs | length xs == 0 = ... which is terrible in multiple ways. But when there's no compelling practical issue, just use whichever makes the code clearer.

As an example, consider these functions (that aren't really doing anything useful, just serving as illustrations):

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 * ...

In f1, the separate patterns emphasize that the recursion has two base cases. In f2, the guards emphasize that 0 is merely a special case for some calculations (most of which are done by calc, defined in a where clause shared by both branches of the guard) and doesn't change the structure of the computation.

尛丟丟 2024-11-16 03:15:33

@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 甚至将匹配案例的字符串共用起来。

@Dan is correct: it's basically a matter of personal preferences and doesn't affect the generated code. This module:

module Test where

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

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

produced the following core:

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
    }
    }

Exactly the same, except for the different default case, which in both instances is a pattern match error. GHC even commoned-up the strings for the matched cases.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文