本文將列舉不同程式語言的幾個小例子,嘗試解釋什麼是列表解析。並且試圖宣揚我的觀點:“列表解析”不論從概念上還是技術上,完全是函數語言程式設計的一根闌尾——多餘而且某種程度上還是有害的。
何為列表解析?
以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的風格。