函数式编程:啥是“不正确的列表”?

Posted

技术标签:

【中文标题】函数式编程:啥是“不正确的列表”?【英文标题】:Functional Programming: what is an "improper list"?函数式编程:什么是“不正确的列表”? 【发布时间】:2010-12-27 12:10:03 【问题描述】:

有人能解释一下什么是“不适当的列表”吗?

注意:谢谢大家!你们都摇滚!

【问题讨论】:

我知道 [1|2] 在 Erlang 中是一个不正确的列表。其他语言中是否也存在不正确的列表? @Philippe,我猜是的,我认为您所描述的是 Erlang/Prolog 模拟我建议的答案。 你可能应该选择罗伯特·维丁的答案,因为他真的知道他在说什么:( 【参考方案1】:

我认为@Vijay 的答案是迄今为止最好的答案,我只是打算对它进行 Erlangify。

Erlang 中的对(cons 单元格)写为[Head|Tail],nil 写为[]。头部和尾部是什么没有限制,但是如果你使用尾部来链接更多的 cons 单元格,你会得到一个 list。如果最后的尾部是[],那么你会得到一个正确的列表。正确的列表对列表有特殊的语法支持

[1|[2|[3|[]]]]

写成

[1,2,3]

还有不合适的列表

[1|[2|[3|4]]]

写成

[1,2,3|4]

所以你可以看到不同之处。与适当/不适当的列表进行匹配也很容易。所以一个长度函数len 用于正确的列表:

len([_|T]) -> 1 + len(T);
len([]) -> 0.

我们明确匹配终止[]。如果给出不正确的列表,这将产生错误。虽然返回列表最后一个尾部的函数last_tail 也可以处理不正确的列表:

last_tail([_|T]) -> last_tail(T);
last_tail(Tail) -> Tail.                 %Will match any tail

请注意,构建一个列表或与之匹配,就像您通常对[Head|Tail] 所做的那样,检查尾部是否为列表,因此在处理不正确的列表时没有问题。很少需要不适当的列表,尽管您可以用它们做一些很酷的事情。

【讨论】:

Lisps 中存在相同类型的结构。如果我不得不猜测,我会认为这就是 Erlang 的来源。 Lisp 中“[a|b]”格式的语法是“(a . b)”。 由于历史原因,Erlang 的列表语法与 Prolog 的相同。 @CMCDragonkai:你可以使用 cons 对所有结构进行编码,就像五十多年前 Lisp 巧妙地展示的那样。因此,您可以在不正确的列表中编码任何内容。当我写任何东西时,我的意思是字面上的任何东西。但是为了可读性、维护和心理健康,在 Erlang 中使用元组更方便。元组具有持续访问权限,这使得它们在某些情况下更加高效。【参考方案2】:

我认为使用 Scheme 更容易解释这一点。

列表是一串以空列表结尾的对。换句话说,一个列表以 cdr 为 () 的 pair 结尾

(a . (b . (c . (d . (e . ()))))) 

;; same as

(a b c d e)

不以空列表结尾的对链称为不正确列表。请注意,不正确的列表不是列表。列表和点符号可以组合起来表示不正确的列表,如下面的等价符号所示:

 (a b c . d)
 (a . (b . (c . d)))

导致构建不正确列表的常见错误示例如下:

scheme> (cons 1 (cons 2 3))
(1 2 . 3)

注意 (1 2 . 3) 中的点——就像 (2 . 3) 中的点,表示一对的 cdr 指向 3,而不是另一对或 '()。也就是说,它是一个不正确的列表,而不仅仅是一个配对列表。它不符合列表的递归定义,因为当我们到达第二对时,它的 cdr 不是列表——它是一个整数。

Scheme 打印出列表的第一部分,就好像它是一个普通的 cdr 链接列表,但是当它到达末尾时,它无法做到这一点,所以它使用了“点符号”。

您通常不必担心点符号,因为您应该使用正常列表,而不是不正确的列表。但是,如果您在 Scheme 打印出一个数据结构时看到了一个意外的点,那么很可能您使用了 cons 并给它一个非列表作为它的第二个参数——除了另一个对或 () 之外的东西.

Scheme 提供了一个方便的过程来创建适当的列表,称为 listlist 可以接受任意数量的参数,并按照这些元素的顺序构造一个适当的列表。您不必记住提供空列表 --- list 会自动以这种方式终止列表。

Scheme>(list 1 2 3 4)
(1 2 3 4)

礼貌:An Introduction to Scheme

【讨论】:

@Vijay:非常感谢您的贡献。我只是碰巧更喜欢 Erlang。干杯。 "列表是一串以空列表结尾的对。请注意,不正确的列表不是列表。" -- 在Scheme,但不是在 Common Lisp 中,不正确的列表被标准视为列表。我不确定 Erlang。【参考方案3】:

Erlang 中列表的定义在 in the manual 中给出 - 特别是第 2.10 节

在 Erlang 中,您真正需要了解的关于不正确列表的唯一一件事就是如何避免它们,而做到这一点的方法非常简单 - 完全取决于您要构建列表的第一件事在。以下所有内容都创建了正确的列表:

A = [].
B = [term()].
C = [term(), term(), term()].

在所有这些情况下,语法 确保有一个隐藏的“空”尾匹配到“[]”结尾。 ..

因此,以下操作都可以生成正确的列表:

X = [term() | A].
Y = [term() | B].
Z = [term() | C]. 

它们都是将新头添加到正确列表的操作。

有用的是您可以将XYZ 中的每一个输入到如下函数中:

func([], Acc)      -> Acc;
func([H | T], Acc) -> NewAcc = do_something(H),
                      func(T, [NewAcc | Acc]).

当尾部的 hidden 空列表仅剩时,它们将撕开列表并终止于顶部子句。

当您的基本列表制作不正确时,问题就出现了,如下所示:

D = [term1() | term2()]. % term2() is any term except a list

这个列表没有hidden 空列表作为终端尾部,它有一个术语...

正如罗伯特·维丁在 cmets 中指出的那样,从这里向下是肉末

那么你如何为它写一个终结子句呢?

让人生气的是无法通过检查来判断列表是否不合适......打印出该死的东西看起来不错...... . 所以你最终创建了一个不正确的基本列表,在上面做一些事情,传递它,然后突然kabloowie你在错误所在的地方发生了几英里的崩溃,你拉着头发尖叫着喊...

但是应该使用dialyzer为你嗅出这些小野兽。

道歉

根据罗伯特的评论,我尝试打印出一个不正确的列表,你瞧,很明显:

(arrian@localhost)5>A = [1, 2, 3, 4].
[1,2,3,4]
(arrian@localhost)5> B = [1, 2, 3 | 4].
[1,2,3|4]
(arrian@localhost)6> io:format("A is ~p~nB is ~p~n", [A, B]).
A is [1,2,3,4]
B is [1,2,3|4]

我曾经花了一些时间寻找一个不合适的列表,并说服自己它是不可见的,嗯 Ah ken noo

【讨论】:

可以查看列表是否不合适,您可以与之匹配,请参阅我的评论。 尽管如此,你仍然可以得到我的 +1 :-)【参考方案4】:

要了解什么是不当列表,您必须首先了解正确列表的定义。

具体来说,列表的“巧妙发现”是您可以仅使用具有固定数量元素的表单来表示列表,即:

;; a list is either 
;; - empty, or
;; - (cons v l), where v is a value and l is a list.

这个“数据定义”(使用 How To Design Programs 的术语)有各种 不错的属性。最好的方法之一是,如果我们在数据定义的每个“分支”上定义函数的行为或含义,就可以保证不会遗漏任何一个案例。更重要的是,像这样的结构通常会导致非常干净的递归解决方案。

经典的“长度”示例:

(define (length l)
  (cond [(empty? l) 0]
        [else (+ 1 (length (rest l))]))

当然,Haskell 中的一切都更漂亮:

length []    = 0
length (f:r) = 1 + length r

那么,这与不正确的列表有什么关系?

嗯,一个不正确的列表使用了这个数据定义,而不是:

;; an improper list is either
;; - a value, or
;; - (cons v l), where v is a value and l is an improper list

问题是这个定义会导致歧义。特别是,第一种和第二种情况重叠。假设我为一个不正确的列表定义了“长度”:

(define (length l)
  (cond [(cons? l) (+ 1 (length (rest l)))]
        [else 1]))

问题是我已经破坏了一个好的属性,如果我取两个值并将它们放入一个不正确的列表中(cons a b),结果的长度为 2。要了解原因,假设我考虑值 (cons 3 4) 和 (cons 4 5)。结果是 (cons (cons 3 4) (cons 4 5)),它可能被解释为 either 为包含 (cons 3 4) 和 (cons 4 5) 的不正确列表, 作为包含 (cons 3 4)、4 和 5 的不正确列表。

在具有更严格类型系统的语言(例如 Haskell)中,“不正确列表”的概念没有多大意义;您可以将其解释为一种数据类型,其基本情况包含两个内容,这也可能不是您想要的。

【讨论】:

【参考方案5】:

我认为它可能指的是 LISP 中的“点对”,例如一个列表,其最终 cons 单元格在 cdr 中有一个原子,而不是对另一个 cons 单元格或 NIL 的引用。

编辑

***建议循环列表也算作不当。见

http://en.wikipedia.org/wiki/Lisp_(programming_language)

并搜索“不正确”并检查脚注。

【讨论】:

@Brian:感谢您的参考。我仍然不确定这意味着什么。 在非静态类型语言中,“列表”只是一串 2 元组(cons 单元),通常将 2 元组或“nil”作为“第二个”字段(以及一些任意数据作为“第一”字段)。此上下文中的“不正确”列表是其最后 2 元组违反此规则的列表。 我听不懂 - 我没有 LISP 背景,只有 Erlang。感谢您在我密集的大脑上努力:-)【参考方案6】:

我会说不正确列表的含义是列表的递归处理将不匹配典型的终止条件。

例如,假设您在 Erlang 中将以下 sum 称为不正确的列表:

sum([H|T]) -> H + sum(T);
sum([]) -> 0.

然后它会引发异常,因为最后一个尾部不是空列表,而是一个原子。

【讨论】:

您可以包含 sum(V) -> V. 以按预期方式处理不正确的列表。但是,我相信标准库的函数假设有一个适当的列表。【参考方案7】:

在 Common Lisp 中,不正确的列表被定义为:

具有非 NIL 终止“原子”的点列表

例子

  (a b c d . f)

循环列表

例子

  #1=(1 2 3 . #1#)

【讨论】:

【参考方案8】:

列表由单元组成,每个单元由两个指针组成。第一个指向数据元素,第二个指向下一个单元格,或者在列表末尾为 nil。

如果第二个不指向单元格(或 nil),则列表不正确。函数式语言很可能允许您构造单元格,因此您应该能够生成不正确的列表。

在 Erlang(可能还有其他 FP 语言)中,您可以通过将 2 元组存储为不正确的列表来节省一些内存:

2> erts_debug:flat_size(1,2).
3
3> erts_debug:flat_size([1|2]).
2

【讨论】:

【参考方案9】:

在 Erlang 中,正确的列表是 [H|T]

H 是列表的头部,T 是列表的其余部分,作为另一个列表。

不正确的列表不符合此定义。

【讨论】:

那么,1 在 Erlang 中是一个不恰当的列表吗?【参考方案10】:

在 erlang 中,正确的列表是单链表。不正确的列表是最后一个节点不是真正的列表节点的单链表。

适当的列表

[1, 2, 3] 就像

不恰当的列表

[1, 2 | 3] 就像

【讨论】:

以上是关于函数式编程:啥是“不正确的列表”?的主要内容,如果未能解决你的问题,请参考以下文章

作为函数式编程语言的 Python

函数式编程中的数组问题

函数式编程--为什么要学习函数式编程?

函数式编程和非函数式编程

第36期函数式编程和响应式编程资料汇总

RxJS 与 函数式编程 - 函数式编程