Haskell (:) 和 (++) 的区别

Posted

技术标签:

【中文标题】Haskell (:) 和 (++) 的区别【英文标题】:Haskell (:) and (++) differences 【发布时间】:2010-12-21 12:45:19 【问题描述】:

对于这样的问题,我很抱歉。我不太清楚 Haskell 中 :++ 运算符的区别。

x:y:[] = [x,y]  

还有

[x] ++ [y] = [x,y]

至于我提出这个问题的反向功能,

reverse ::[a]->[a]
reverse [] = []
reverse (x:xs) = reverse(xs)++[x]

为什么以下方法不起作用?

reversex ::[Int]->[Int]
reversex [] = []
reversex (x:xs) = reversex(xs):x:[]

给出类型错误。

【问题讨论】:

附带说明,您可以(并且应该)在不使用括号的情况下调用:reverse (x:xs) = reverse xs ++ [x],否则当您使用具有多个参数的函数时会被绊倒。 不要调用像func(arg)这样的函数。那是可怜的哈斯克尔。总是调用像func arg 这样的函数。带有清晰空格的代码使代码更加自信和可读。 @AJFarmar func arg 确实比 func(arg) 更正确 Haskell,但我认为 f(x) 通常比 f x 更具可读性,因为它与大多数其他语言以及指定函数的数学方法。所以我想说“更自信、更易读”是一个见仁见智的问题。 @icc97,上下文。在编写 Haskell 代码的上下文中,func(arg) 与使用 "'goes down to' operator" in C 一样令人困惑 【参考方案1】:

您可以稍作更改并获得正确的结果。

reversex ::[Int]->[Int] # comment this line
reversex [] = []
reversex (x:xs) = reversex(xs) ++ x : []

【讨论】:

【参考方案2】:

cons 往往是类型构造函数而不是运算符。这里的例子是: 可以在let..in.. 表达式中使用,但++ 不是

let x : xs = [1, 2, 3] in x -- known as type deconstructing

将返回 1 但

let [x] ++ [y, z] = [1, 2, 3] in x

将返回错误Variable not in scope x

为方便起见,请像这样考虑cons

data List a = Cons a (List a) -- is equvalent with `data [a] = a:[a]`

https://en.wikibooks.org/wiki/Haskell/Other_data_structures

此外,如果您想使用 cons 反转数组。这里举个例子,知识取自Prolog

import Data.Function

reversex1 [] = []
reversex1 arr = reversex arr []

reversex [] arr = arr
reversex (x:xs) ys = reversex xs (x:ys)

main = do
    reversex1 [1..10] & print

【讨论】:

【参考方案3】:

: 运算符称为“cons”运算符,用于将头元素添加到列表中。所以[] 是一个列表,x:[]x 添加到空列表前面,从而形成列表[x]。如果你然后 cons y:[x] 你最终得到列表 [y, x]y:x:[] 相同。

++ 运算符是列表连接运算符,它将两个列表作为操作数并将它们“组合”成一个列表。因此,如果您有列表[x] 和列表[y],那么您可以像这样连接它们:[x]++[y] 得到[x, y]。

注意: 接受一个元素和一个列表,而++ 接受两个列表。

至于你的代码不起作用。

reversex ::[Int]->[Int]
reversex [] = []
reversex (x:xs) = reversex(xs):x:[]

reverse 函数的计算结果是一个列表。由于: 运算符不将列表作为其第一个参数,因此reverse(xs):x 无效。但是reverse(xs)++[x] 是有效的。

【讨论】:

【参考方案4】:

与 (++) 的连接

也许我正在考虑深入研究,但是, 据我了解,如果您尝试连接 例如使用(++) 的列表:

[1, 2, 3] ++ [4, 5]

(++) 必须遍历完整的左侧列表。 看看code of (++)就可以了 更加清晰。

(++) :: [a] -> [a] -> [a]
(++) []     ys = ys
(++) (x:xs) ys = x : xs ++ ys

因此,最好避免使用(++),因为每次调用 reverse(xs)++[x] 列表越来越大(或越来越小,取决于 上的观点。无论如何,程序只需要遍历另一个 每次通话都列出)

示例:

假设我通过串联实现了反向。

reversex ::[Int]->[Int]
reversex [] = []
reversex (x:xs) = reversex(xs)++[x]

反转列表 [1, 2, 3, 4] 看起来有点像这样:

reversex [1, 2, 3, 4]
reversex [2, 3, 4]               ++ [1]
reversex [3, 4]           ++ [2] ++ [1]
reversex [4]       ++ [3] ++ [2] ++ [1]
reversex [] ++ [4] ++ [3] ++ [2] ++ [1]
         [] ++ [4] ++ [3] ++ [2] ++ [1]
         [4]       ++ [3] ++ [2] ++ [1]
         [4, 3]           ++ [2] ++ [1]
         [4, 3, 2]               ++ [1]
         [4, 3, 2, 1]

使用 cons 运算符 (:) 进行尾递归!!!

处理调用堆栈的一种方法是添加accumulator。 (并不总是可以只添加一个累加器。但大多数 一个处理的递归函数是primitive recursive,因此可以 转换为tail recursive functions。)

在累加器的帮助下,可以制作这个例子 工作,使用 cons 运算符(:)。 累加器——在我的例子中是ys——累加当前结果并作为参数传递。由于累加器,我们现在能够 使用 cons 运算符通过附加 每次都是我们初始列表的首位。

reverse' :: (Ord a) => [a] -> [a] -> [a]
reverse' (x:xs) ys = reverse' xs (x:ys)
reverse' [] ys     = ys

这里有一点需要注意。

累加器是一个额外的参数。不知道Haskell 提供默认参数,但在这种情况下会很好, 因为你总是用一个空列表调用这个函数 像这样的累加器:reverse' [1, 2, 3, 4] []

有很多关于尾递归的文献,我是 肯定有很多类似的问题 StackExchange / ***。如有错误请指正。

亲切的问候,

编辑 1

Will Ness 为感兴趣的人指出了一些指向非常好的答案的链接:

Why are difference lists more efficient than regular concatenation? Haskell foldl' poor performance with (++)

编辑 2

好的。 感谢 dFeuer 和他的更正,我想我了解 Haskell 好一点。

1.$! 超出我的理解范围。在我所有的测试中,它似乎 让事情变得更糟。

2.正如 dFeuer 指出的: 代表(:) 应用到xy 的thunk 在语义上与x:y 相同,但占用更多内存。所以这对于 cons 运算符来说是特殊的 (和懒惰的构造函数),没有必要以任何方式强制执行。

3.如果我使用非常相似的函数来对列表的整数求和, 通过 BangPatterns 或 seq 函数进行严格评估 如果使用得当,将防止堆栈变得太大。例如:

sumUp' :: (Num a, Ord a) => [a] -> a -> a
sumUp' (x:xs) !y = reverse' xs (x + y)
sumUp' [] y      = y

注意 y 前面的爆炸。我在ghci中试过了 占用更少的内存。

【讨论】:

Haskell 从不延迟延迟延迟构造函数的应用(因为这样做从来没有任何优势),因此没有必要也没有优势,手动强制这些结果。您甚至可能会通过强制执行已经评估的内容来减慢速度! @dfeuer 我不太确定 Haskell 的严格性。 wiki.haskell.org/Performance/Accumulating_parameter 虽然确实建议人们应该考虑对累加器使用严格的评估:“我们需要解决一个关于堆栈溢出的小问题。当我们传递列表时,该函数将累积 (1 + acc) thunk。一般来说,使你的累加器参数严格是有意义的,因为它的值将在最后需要。" GHC 一般会延迟函数应用和模式匹配,但不会延迟构造函数应用。原因是表示(:) 应用到xythunk 在语义上与x:y 相同,但占用更多内存。构造函数是特殊的。严格的构造函数并没有那么特别。 但我之前的观点是,如果我通常调用f (g x y),GHC 将创建一个代表g x y 的thunk 并将该thunk 传递给f。但是如果g 是一个惰性构造函数(即一个构造函数的定义不使用!),GHC 将改为直接应用g 并将结果传递给f。语义相同,但速度更快且节省内存。 我喜欢在 Haskell 中指向 ++ 函数的源代码是很自然的,在我看来,你很少会在其他语言中看到这一点,例如我从来没有想过要在说 javascript 中查看 concat 的内部定义,也从来没有见过任何人或任何 JS 书籍推荐查看源代码【参考方案5】:

:将一个元素组合到一个列表中。

++ 附加两个列表。

前者有类型

a -> [a] -> [a]

而后者有类型

[a] -> [a] -> [a]

【讨论】:

对于 Lisp-vocabulary-challenged,"cons" 构造一个新的列表节点并将其添加到列表的头部。

以上是关于Haskell (:) 和 (++) 的区别的主要内容,如果未能解决你的问题,请参考以下文章

Haskell中`mod`和`rem`之间的区别

源码ImProve:嵌入Haskell的命令式编程语言

Haskell中`data`和`newtype`之间的区别

Rust 闭包和 Haskell lambda 有啥区别? [关闭]

Rust闭包和Haskell lambda有什么区别? [关闭]

Haskell:(Num a,Ord a)和(Integral a)之间的区别