为什么表达式在Haskell中不受欢迎?

sim*_*ays 24 haskell pattern-matching

这是一个我一直想知道的问题.如果语句是大多数编程语言中的主要语言(至少是我曾经使用过的语句),但在Haskell中,它似乎很不受欢迎.我理解,对于复杂情况,Haskell的模式匹配比一堆ifs更清晰,但是有什么真正的区别吗?

举一个简单的例子,拿一个自制版本的总和(是的,我知道它可能只是foldr (+) 0):

sum :: [Int] -> Int
-- separate all the cases out
sum [] = 0
sum (x:xs) = x + sum xs

-- guards
sum xs
    | null xs = 0
    | otherwise = (head xs) + sum (tail xs)

-- case
sum xs = case xs of
    [] -> 0
    _ -> (head xs) + sum (tail xs)

-- if statement
sum xs = if null xs then 0 else (head xs) + sum (tail xs)
Run Code Online (Sandbox Code Playgroud)

作为第二个问题,哪一个选项被认为是"最佳实践",为什么?我的教授回来的时候总是尽可能地使用第一种方法,我想知道这是否只是他的个人偏好或者背后有什么东西.

ham*_*mar 47

你的例子的问题不是if表达式,而是使用像head和这样的部分函数tail.如果您尝试使用空列表调用其中任何一个,则会抛出异常.

> head []
*** Exception: Prelude.head: empty list
> tail []
*** Exception: Prelude.tail: empty list
Run Code Online (Sandbox Code Playgroud)

如果在使用这些函数编写代码时出错,则在运行时才会检测到错误.如果模式匹配出错,程序将无法编译.

例如,假设你不小心切换thenelse你的函数的部分.

-- Compiles, throws error at run time.
sum xs = if null xs then (head xs) + sum (tail xs) else 0

-- Doesn't compile. Also stands out more visually.
sum [] = x + sum xs
sum (x:xs) = 0
Run Code Online (Sandbox Code Playgroud)

请注意,您的示例与警卫具有相同的问题.

  • @swanysims:`head`,`null`和`tail`都是使用模式计算实现的,它们是内联的主要候选者.GHC的优化器将把两者都变成完全相同的代码,因此请关注使代码更易于阅读,理解和使用的原因. (2认同)

ert*_*tes 20

我认为布尔盲目性文章很好地回答了这个问题.问题是布尔值一旦构造就失去了所有的语义.这使得它们成为错误的重要来源,也使代码更难理解.


Ing*_*ngo 16

您的第一个版本是您的教授首选版本,与其他版本相比具有以下优势:

  1. 没有提到 null
  2. 列表组件在模式中命名,因此没有提及headtail.

我认为这个被认为是"最佳实践".

有什么大不了的?我们为什么要避免尤其是headtail?好吧,每个人都知道这些功能并不完全,所以人们会自动尝试确保涵盖所有案例.[]上的模式匹配不仅突出,而且null xs编译器可以检查一系列模式匹配的完整性.因此,具有完全模式匹配的惯用版本更容易掌握(对于训练有素的Haskell读取器)并且由编译器证明是穷举的.

第二个版本略好于最后一个版本,因为人们立即看到所有案例都得到处理.但是,在一般情况下,第二个等式的RHS可能更长,并且可能存在具有几个定义的where子句,最后一个可能是这样的:

where
   ... many definitions here ...
   head xs = ... alternative redefnition of head ...
Run Code Online (Sandbox Code Playgroud)

要绝对明白RHS的作用,必须确保通用名称没有被重新定义.

第三个版本是最糟糕的一个恕我直言:a)第二场比赛无法解构列表仍然使用头尾.b)该案例比具有2个等式的等效符号略微冗长.


Mat*_*hid 8

在许多编程语言中,if语句是基本原语,而switch-blocks之类的东西只是使深度嵌套的if语句更容易编写的语法糖.

Haskell反过来做到了.模式匹配是基本原语,if-expression实际上只是模式匹配的语法糖.类似地,构造类似于null并且head仅仅是用户定义的函数,它们最终都使用模式匹配来实现.所以模式匹配就是最底层的事情.(因此可能比调用用户定义的函数更有效.)

在许多情况下 - 例如你上面列出的那些 - 只是风格问题.编译器几乎可以肯定地将所有版本的性能大致相等.但通常 [并非总是!]模式匹配使您更清楚地了解您要实现的目标.

(编写一个if-expression非常容易,你可以用错误的方式得到两个替代方案.你会认为这是一个罕见的错误,但它出奇的普遍.通过模式匹配,几乎没有机会犯这个特定的错误虽然还有很多其他事情要搞砸了.)


Wil*_*ess 6

每次调用null,headtail需要模式匹配.但是你的答案中的第一个版本只进行了一次模式匹配,并通过模式的命名组件重用其结果.

只是为了那个,它更好.但它在视觉上更明显,更具可读性.


Eri*_*ikR 5

由于(至少)以下原因,模式匹配优于if-then-else语句的字符串:

  1. 它更具说服力
  2. 它与sum-types很好地相互作用

模式匹配有助于减少代码中"意外复杂性"的数量 - 也就是说,代码实际上更多地是关于实现细节而不是程序的基本逻辑.

在大多数其他语言中,当compier/run-time看到一串if-then-else语句时,它别无选择,只能按照程序员指定的顺序测试条件.但模式匹配鼓励程序员更多地关注描述应该发生什么以及应该如何执行.由于Haskell中值的纯度和不变性,编译器可以将模式集合视为一个整体,并决定如何最好地实现它们.

类比将是C的switch声明.如果转储各种switch语句的汇编代码,您将看到有时编译器将生成一个链/树比较,而在其他情况下,它将生成一个跳转表.程序员在两种情况下都使用相同的语法 - 编译器根据比较值选择实现.如果它们形成连续的值块,则使用跳转表方法,否则使用比较树.如果检测到比较值中的其他模式,这种关注点的分离允许编译器在将来实现更多策略.