计算机语言设计:列表的理解以及为什么它是有害的

本网站用的阿里云ECS,推荐大家用。自己搞个学习研究也不错
本文将列举不同编程语言的几个小例子,尝试解释什么是列表解析。并且试图宣扬我的观点:“列表解析”不论从概念上还是技术上,完全是函数式编程的一根阑尾——多余而且某种程度上还是有害的。

何为列表解析?

以python中的列表解析为例:

1
S = [2*n for n in range(0,9) if ( (n % 2) == 0)] print S # prints [0, 4, 8, 12, 16]


它产生一个0到8的列表,将奇数从该列表中移除,最后将剩余元素乘2,最后返回所得列表。

Python的列表解析(LC)的语法如下:

1
[myExpression for myVar in myList if myPredicateExpression]

总而言之,这种特殊语法产生一个列表,并且允许程序员对其中元素进行过滤,以及将其中元素作为参数传给一个函数,但是所有这些都已“表达式”的形式出现。

(译者注:这个语法本身也是一个“表达式”,所以可以嵌套使用。)

 

列表解析的函数式的写法是这样的:

1
map( f, filter(list, predicate))

其他语言的列表解析是相似的。这儿有几个来自维基百科的例子。在下面的例子里,x^2>3作为条件,然后把每个元素乘以2返回结果.

Haskell

1
s = [ 2*x | x < [0..], x^2 > 3 ]

F#

1
seq { for x in 0..100 do if x*x > 3 then yield 2*x } ;;

OCaml

1
[? 2 * x | x < 0 max_int ; x * x > 3 ?];;

Clojure

1
(take 20 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))

Common Lisp

1
(loop for x from 1 to 20 when (> (* x x) 3) collect (* 2 x))

Erlang

1
S = [2*X || X < lists:seq(0,100), X*X > 3].

Scala

1
val s = for (x < Stream.from(0); if x*x > 3) yield 2*x

这里是维基百科对
List comprehension
的解释,引用如下:

A list comprehension is a 
syntactic construct
 available in some programming languages for creating a list based on existing lists.

列表理解(LC)有以下特征:

*1.一个直接的列表生成器,可以对元素进行过滤,并且对每个元素应用一个函数

*2.是某些语言里面的特殊语法

*3.这种语法是一个单独的表达式,而不是由单独的函数组成

为什么列表理解是有害的?

  • 列表理解就像一个不透明的俚语一样,它妨碍沟通,造成误会
  • 列表理解是编程里面一个冗余的概念。它只是一个简单的列表生成器。他可以被简单的功能函数formmap(func,filter(list,predicate))代替,或者被一些语句代替,比如perl:for (0..9) { if ( ($_ % 2) == 0) {push @result, $_*2 }}.
  • 这种存在于多种语言中的特殊语法,其实不是必要的。如果需要这样的函数,那么它可以直接是一个一般的函数,比如LC(function,list,predicate).

列表解析语法并不是很有必要。一个更好更一致的方法是用函数式语言的精髓,使用普通函数的组合。

1
map( f, filter(list, predicate))

这是python的语法
:

1
map(lambda x: 2*x , filter( lambda x:x%2==0, range(9) ) ) # result is [0, 4, 8, 12, 16]

在Mathematica中,可以这样写

1
Map[ #*2

在Mathematica中,算术操作符可以不使用Map而直接映射到列表,因此上面的代码可以这样写:

1
Select[Range@9, EvenQ] * 2

还可以写成线性前缀风格:

1
(#*2

或者线性后缀风格:

1
9 // Range  // (Select[#, EvenQ]

在上面,我们就像Unix里的管道那样排列函数在一起。我们从9开始,使用“Range”来获取一个1到9的列表,然后使用一个函数来过滤出偶数,接着我们使用一个函数来把过滤出来的每个数字乘以2。符号“//”是一个后缀符号,类似于bash(shell)的“|”符号,同时,“@”是一个与“|”相反的符号。

(☛ Short Intro of Mathematica For Lisp Programers)

无需特殊语法的列表理解函数

在函数式语言中,假如我们想要“列表解析”这一特性。通常地,默认情况这可以这样做

1
map(func, filter(inputList, Predicate))

但这种用法会很频繁,我们想为此创建一个更方便的函数。作为一个独立的函数,它更容易被编译器优化。因此,我们可以创建一个函数LC像这样:

1
LC(func, inputList, Predicate)

这个关系到一种语言是否应该创建一个更方便的新函数,否则就需要3个函数的组合。Common Lisp和Scheme Lisp是极端对立的典型例子。

注意,这里没涉及到新的语法。

假设,某人对下面的有争论:

实际上, 这个语法:

1
[x+1 for x in [1,2,3,4,5] if x%2==0]

远比这个语法方便:

1
map(lambda x:x+1,filter(lambda x:x%2==0,[1,2,3,4,5]))

 

那么这个:

1
LC(func, inputList, P)

与以下的比较

 

1
[func for myVar in inputList if P]

这函数式格式:

  • 更短小
  • 没有其他特别的新语法

 

创建一个新函数的问题和决策

假设我们决定通过过滤器生成列表,这个操作很频繁,是值得创建的一个函数。

1
LC(func, inputList, Predicate)

现在,在函数式语言中,一般的设计原则是你应该减少函数的数目,除非你真的需要。因为在你的语言里相关函数的任何组合列表可能成为一个新的函数。因此,如果我们确实认为列表解析是有用的,我们可能会
推广到最大限度地发挥一个函数的作用。

举个例子,我们会考虑是不是值得增加第四个参数来让用户指定只返回前n个元。如

1
LC(func, inputList, Predicate, n)

那么再将它分割成m个子列表呢?

1
LC(func, inputList, Predicate, n, m)

要是再将它分割成更广泛通用的呢?即弄成m、m1、m2等等。

1
LC(func, inputList, Predicate, n, list(m,m1,m2,))

还有排序?也许你用到列表的时候这些功能总是需要一起使用的。

1
LC(func, inputList, Predicate, n, list(m,m1,m2,), sortPredcate)

当我们需要将一个函数放进列表中,有时候我们会想映射到每个分支。(就像Common Lisp中的map),所以我们可能还需要有其它的可选参数,例如:

1
2
LC(func, inputList, Predicate, n, list(m,m1,m2,), sortPredcate,
mapBranch:True)

如果…

所以,在默认情况下,通过一个或多个函数的序列,上面这些或这些的组合将被编程语言处理(即组成)。 但当我们创建一个新的函数时,我们应当仔细权衡它是否必要,要知道,若不这样的话,语言将成为装满了非比要的,混乱的函数的袋子。

因此现在的问题是,难道通过列表解析生成一个列表的确是一个非常频繁使用的动作吗?如果是的话,那为什么我们要去创建一种特殊的语法,诸如[expr for var in list if P]这样,而不是使用一个函数LC(func,list,P)呢?

同时注意一下,上面列出的函数LC并非一个强大到可以生产任意嵌套列表的函数。如果大家想了解一下更强大列表生成函数,那这里有一个例子,它可以生成任意嵌套的树状结构列表,参见 Mathematica 的 Table函数:

 

列表解析,特定语法,命令式语言

列表解析真正的“优势”在于其特殊的语法,及由命令式语言所组成的特定语法。这是因为在命令式语言中,每个结构都有可能有一大堆的特定语法和关键字。这非常常见,比如:i++,++i,for(;;){},while(){},0x123,expr1 ? expr2 : expr3,sprint(…%s…,…), ….

对于那些发现命令式语言语法的好处的人来说,由于“列表解析”为语言添加了额外的独特语法,它也许也是有益处的。特定语法和冗长的关键字可以帮助人们理解代码的含义。举个例子: 在语法[… for … in …]中,程序员可以通过方括号来得知其值为一个列表(list),并且关键字for和in帮助程序员了解该结构的部分用途。如果使用函数式语法LC(…, …, …),就没有这些提示了,则程序员必须理解函数的参数才行。

错误术语以及如何判定

有人这样写道:

术语“列表解析”是直观的,它是基于数学符号。

术语“列表解析”是含糊不清的。它阻碍了交流并且增加了误会。更好的名字是“列表生成器”。

你凭什么说“列表解析”是直观的?有没有任何统计、调查、研究和参考?

要将它放在特定的语境中,你能说“ lambda”也是直观的吗?“let”是直观的?“for”是直观的?“when”是直观的?我的意思是说,给你一些常用的计算机术语来评估,然后告诉我们哪些是好的,哪些是坏的。所以我们要在特定环境下评估你的专业术语。
例如,我们想知道,以你的观点这些术语好吗:
currying (偏函数),
lisp1 lisp2
tail recursion (尾递归),
closure (闭包),subroutine(子程序),command(命令),
object (对象)。或者,也许是让你论述一下“module”、“package”、“add-on”和“library”相比较下各自的优点和意义。我想要了解你对此的观点的话,至少 每项都得 要有几段分析吧。如果你叙述或写了关于此话题千字以上的文章,那么我们 所有人就 都可以 对你在这方面的熟悉和了解程度 做下评估了。

同样,“直观”并不是考虑一个术语是好是坏的唯一方面。举例来说,emacs中所使用的术语“frame”。它非常直观,因为frame是一个普遍的英语单词,任何人都认识。我们有门框、窗框、图框中的“frame”,都与电脑中emacs中的“frame”意思相近。无论如何,由于历史原因,通常在电脑软件中我们称之为“window”,并且碰巧术语“window”同样在emacs中有技术含义,我们今天称它“split window”或“frame”。所以,在emacs中术语“frame”和“window”的概念是混淆的,因为emacs的“frame”就是我们叫“window”的东西,同样emacs中的“window”被我们称作frame。这个例子告诉我们,即便当一个术语非常直观,它可能仍然是糟糕的。

作为另一个例子,目标人群对被使用术语的通常理解,也是一个重要的方面。举例来说,术语“lambda”,它是一个希腊字母,并不能很好地传达它所起的作用。这个单词本身的意义并不会使人联想到函数的概念。字母“λ”偶然地被一位逻辑学家在他的名为“lambda演算”的研究中当作速记标记来使用(其中“演算”部分作为系统科学在17世纪的基本术语,特别是有关于机器推理方面)。无论如何,术语“lambda”以这种方式使用在计算机科学与编程中已经很久并且非常广泛,近期历史大约有50年(如果我们溯本遂源会更久)。所以,由于已经被使用,实际上它已经成了普遍用法,这样它便减弱我们认为它是一个坏术语的程度。即便如此,请记住那仅仅因为一个术语已经被使用,但要是术语本身在其它方面非常糟的话,它可能仍需要被改变。由于这个原因,这些术语对新生代而言会导致学习曲线问题。

看到了吧,当你判断一个术语的时候,你不得不考虑许多方面。这是相当棘手。当你判断这些”行话”的时候,你就会有这样的疑问:

• 这个术语是否准确传达了它的本意?(即作为一个单词是否有效地促进交流)

• 社会上的其他人都理解这个术语吗? (比如 更科学的来讲:占多大比例?)

 

以上每个简单的问题都牵涉到很复杂的东西。比如,它需要:

  • 语言专家(很多相关子领域:语用学、语言历史、词源学)在这些领域的专家可以恰当地给我们一些线索,从而判断一个新术语在语言理论、实践、历史方面是否合适。
  • 该领域的实践经验(编程或计算机科学)。在工业环境中、从他们日常读写文档的经验、在他们与其它程序员的交流中,使计算机程序员具备专业知识以便可以告诉我们哪个术语是好,哪个是坏。
  • 学术专家(例如教育者,教授,编程书籍作者/老师)。老师,以一个传授知识给学生的角度告诉告诉我们一个术语的用处。举例来说,老师可以告诉我们哪些术语经常让学生感到困惑。
  • 科学调查,社交科学。从理论或实践的科学研究,能以数据或其它科学方式告诉我们一个术语质量的好坏。

此外,您可能不知道,其实有一些专业科学家专门制造专业术语“O认为这是很好的,因为对我来说直观的

致谢

感谢 w_a_x_man 提供一下这些 ruby 代码:

1
(0..9).select{|n| n.even?}.map{|n| 2*n}

注意,这是没有列表理解因为它不使用特殊的语法但它符合Ruby的风格

转载自演道,想查看更及时的互联网产品技术热点文章请点击http://go2live.cn

未经允许不得转载:演道网 » 计算机语言设计:列表的理解以及为什么它是有害的

赞 (0)
分享到:更多 ()
已有 0 条评论 新浪微博