(scm 開場)
Guest
那我們下午的演講要開始囉。今天很高興可以請到唐鳳來做這個主題的演講。唐鳳,大家知道他非常有名,做過很多很多事情。不過對我們這個圈子,讓大家印象最深刻的是那個 Perl 6 ,*,其實當時我知道的情形大概是那個語言定出來了,但是沒有人知道怎樣實作它。到最後呢,她先用 Haskell 做了一個 implementation 出來,在我們這個圈子,其實是非常轟動的事情。「 Functional language 有用了!」這樣。所以她今天會談一談 functional language 的用處。此外*她也*很多非常有趣的計畫,像零時政府啊,等等其他*。待會兒可以留下來跟我們聊聊。謝謝。
(開始)
王映筑
大家好,很高興可以來這裡分享「函數程式設計的商業應用」這個主題。
以前我在給演講的時候,常常因為簡報太多、時間不夠用,而講得太快,造成文字紀錄朋友的困擾,也壓縮到問答的時間。
—
這次演講的題目是 Commercial Uses of Functional Programming,它也是一個國際會議的名字,也就是 CUFP。2005 年第二屆是在愛沙尼亞舉辦,那時我給過一個演講。今年是第十屆了,我也在議程組 (Program Committee) 幫忙規劃內容。
這張照片是瑞典哥德堡 (Gothenburg),今年九月 CUFP 舉辦的地方,風景非常漂亮。
CUFP 在當初辦的時候,就有個口號叫做 Functional Programming as a means, not an end,就是剛才穆老師說的「拿函數程式設計來用、當作工具,而不是只是鑽研它本身」,就是說拿它來做實際的事情。
—
哥德堡是個工業城,也是瑞典的第二大城、Volvo 汽車就是從這裡出來的。九月一號,世界上最大的一個函數程式設計的年會 ICFP 會在這裡舉辦三天,接下來三天就是我們的 CUFP。
哥德堡最近在新聞上有出現,它在今天(也就是演講的這個時候)開始了一個為期一年的實驗,就是哥德堡的所有公務員從今天開始,到明年的這個時侯,每天只需要上班六小時。
這個實驗非常有趣。如果成功的話,他們就要推廣到全市,包含商業界,所有人都是上班六小時。他們也挑了一些特定的產業,像是居家照顧之類,把全市分成「對照組」和「實驗組」。對照組保持本來的工時,實驗組每天只要上班六小時,領一樣的薪水,一年結束之後來比較,看對生產力有沒有影響。如果沒有影響就推行到全部產業,如果效果良好,也許就會推行到全瑞典,真是太爽了。[笑聲]
為什麼他們會敢做這個實驗,是因為從 2002 年開始,Toyota 車廠在哥德堡就已經率先採用了六小時的工時。畫面上這位技術人員正在組裝一台 Prius,他的工作是從中午十二點開始,也就是說起床、吃個早飯、騎自行車去上班,一路工作到六點,非常認真工作之後打卡下班,然後就沒事了。他的月薪是新台幣十二萬,比一般的汽車工人要稍微高一些。
這是因為 Toyota 他們發現說,縮成六小時、中間不休息,這樣的方式下,產能其實是提高的,所以他們反而可以給更高的待遇。下圖可能看不太清楚,是 OECD 統計 GDP 和工作時數的關係,我們可以把它放大來看...
—
放大後就可以看出來,工作時數越少、越偏左邊,每小時產出的 GDP 就越高,這是非常標準的一個曲線。台灣在這邊,工作時數還蠻長的,但至少相對比平均來說要有效率,落在曲線上面。但是我們可以看到台灣右邊有一整排國家,上班工作的時間愈來愈長,一天十、十二、十四小時,可是對 GDP 來說,乘起來卻完全沒有影響。花這麼多時間工作,但是 GDP 沒有什麼差別。
為什麼這樣,還有地方要制定標準工時是十小時、十二小時呢?我想是因為「低效率的工作」這件事本身,是會讓人習慣的。長時間消耗在辦公室裡面、即使什麼事也沒做,但是感覺上還是有在做事,其實也是蠻舒服的一件事情...[笑聲]
這就讓我想到,在 1970 年代,C 語言剛出來時,那些原本寫 Lisp 函數程式語言的大師們,都覺得很奇怪:這個表達力很弱、容易出錯、動不動就 buffer overflow、要花非常多力氣來維護的語言,為什麼大家都一窩蜂跑去寫?它寫的作業系統 Unix 那麼多人用,我們花那麼多時間研究出的 Lisp Machine 為什麼沒有人用呢?
當時,一位很有名的 Lisp 黑客 Richard Gabriel,他就寫了一篇 paper,叫做「Worse is Better」,來說表達力弱、容易出錯,這些都不是問題。只要很容易上手,然後讓人覺得一開始寫起來蠻輕鬆的,然後陷入一個習慣當中,之後大家就不會想要去學別的語言了。他就用這個方式,來解釋為什麼 C 語言這麼流行的原因。
到了這個世紀,我們可以看到 JavaScript 是一個非常類似的情況:表達力弱、容易出錯,需要大量時間維護,可是大家都一窩蜂跑去學,不知道為什麼。「Worse is Better」翻成中文,我把它翻成「劣即是夯」,就是說很爛的反而會很流行... [笑聲]
不過「劣即是夯」還可以從另外一個方向來看,我們可以把它讀成「少力就是大力」,在英文叫做「Less is More」。就像 Haskell 這種語言,一開始學的時候有一個陡峭的曲線,還蠻辛苦的,但一旦掌握它之後,就可以用非常少的篇幅,做出非常多的事情,然後效率又很高。所以熟練之後,它就能創造出相當了不起的價值,就是「大力」的意思。這可以看作曲線的最左端,就是瑞典現在的位置。
—
拿這個比喻切入,我們就可以來講「函數程式設計」的商業應用。要講這個之前,應該要先定義一下,什麼是「函數式的程式語言」。一般來講,如果我們去 Wikipedia 或教科書上看,它指的是「支援 Closure 閉包函數,作為參數跟傳回值」,也就是 closures as first-class values,就可以叫做「函數式語言」。
這是什麼意思呢?我們直接來看 code。各位上了兩天課,應該看得懂這段 code… 這是 Haskell 裡面,可能是最重要的一個高階函數(用別的函數當參數的函數),就是 「結合函數」,寫成一個點。這個函數有兩個參數,就是 f 跟 g。它把 f 跟 g 拿進來之後,自己傳回另外一個函數。
這個傳回值是 λx,就是「有一個參數叫做 x」的某個函數,它的定義是 f(g(x))。舉例來講,如果我們結合 (+1) 和 (*2) 的話,它傳回的函數就是「先乘以二、再加一」,如果套用到 3 上面的話,結果就是 3 * 2 + 1,就是 7 嘛。
在這裡,我們要注意的是說,在 λx 的定義裡面除了提到 x,它也是提到 f 和 g,可是並沒有給出 f 跟 g 的定義。f 跟 g 在 λx 沒有定義,這叫作 free variable,就是「自由變數」,它們的定義是從上層函數那邊去給的。也就是說,每次跑到這裡的時候,就會用上層函提供的環境把它「封閉」起來(「close over」它們),把 f 和 g 當時傳進 (.) 結合函數的值,「包」進 λx 函數裡面。這就是為什麼 Closure 翻譯成「閉包」。
IRC 上說剛才的內容都有上到,所以我不用特別多講,那太好了... 但是我要講的是說,這並不是 Haskell 特有的功能,在 JavaScript 裡面,我們也可以寫完全一樣的東西。
這是 JavaScript,我們同樣也可以定義一個 function 叫 compose,吃兩個函數當作參數,然後自己傳回另外一個函數 λx(只是這裡的 λ 要寫成 function),該函數的傳回值是 f(g(x))。除了要多打一些字,可能手會比較痛之外,這兩個是完全一模一樣的事情。
—
用結合函數,把問題拆成各自獨立的一些小部份,再用結合、filter、map 這些高階函數來架構程式,這就是「functional programming 的直覺」。有了這個直覺,就可以看到任何問題的時候,都把它想成一系列小函數,組合起來的結果。如果能夠充份掌握這個方法,就可以達到一個境界:「大事化小、小事化無、以無事取天下」,一切的東西,都化成非常簡單的、吃一個參數傳回一個值的函數,把它們全部組合起來,就可以變成一個非常複雜的程式。
好,傳教到這裡為止。[笑聲]
當然這並不是 Haskell 首創,而是 1958 年從 Lisp 開始的一個想法。到了 1970 年,出現了所謂的「物件導向」語言 Smalltalk,它有很多很有趣的特性,像是「繼承」、「封裝」、「類別」這些大家耳熟能詳的特性。
之後就有兩個傢伙,Steele & Sussman,本來是寫 Lisp 的,他們就在想要怎麼改造 Lisp 語言,讓它有物件導向的特性呢?他們本來以為要加一大堆功能,但是在研究兩年之後,他們就發現不對,只需要加一個功能,就是「由外層函數提供裡面自由變數的閉包」這個功能,因為這是從 lambda calculus 來的,所以稱做 λ(lambda)。
只要有 λ 的功能,就可以實現所有物件導向所提出,看起來非常複雜的功能,這實在是太厲害了。所以他們就寫了一系列「Lambda the Ultimate」的 paper:「λ無上命令式」「λ無上宣告式」「λ無上跳轉式」「λ無上指令集」之類,聽起來很像六字真言的 paper,當然它們非常值得一看。
整個重點是,別的語言能夠做的,我們這邊只要用一個 λ 就可以做完了。他們發明的語言叫 Scheme,它的標誌就是 λ。後來所謂血統純正的「函數式程式語言」,在 logo 裡一定都會放一個 λ。我們可以看到,Haskell 標誌的中間就是一個 λ。左邊是最近很流行的 Clojure,它也是一個 λ。右邊是 Objective Caml,那隻駱駝身上可能有四、五個 λ… [笑聲]
Guest
那到了八零年代的時候,所謂的腳本語言 scripting language ,就是以 Perl 為首,Python 、 Lua 、 Ruby 、 JavaScript 、 Perl 6 ,通通都覺得 λ 是個非常好的主意,所以它們都用各自的方法,把 λ 加進自己的語言裡面,本來沒有的,也都變成有了。
那接下來呢,就是工業界的 D 、 Erlang ,或者是我們在學界比較常用的 Mathematica 、 Matlab 跟最近非常紅的 Julia ,這些都是直接把 λ 當成它們語言建構的基礎。
到了新世紀, 2001 年的時候,微軟發現,再不支援 λ 就完了,就沒有人要用他們家的平台了,所以他們只好開發了 .Net 這套系統,把他們本來的系統語言變成 C# 、 Visual Basic .Net ,以及直接從 OCaml 全部抄過來,高達九成九九相似的 F# 語言,這些語言,也都是函數式程式語言,都支援 λ 。
再過了四五年,突然之間大家發現 JavaScript 到處都是,所以就出現了一系列,讓 JavaScript 當作執行引擎,我們編譯到 JavaScript 的簡寫語言包含 CoffeeScript 、 LiveScript 、 TypeScript 跟 Dart ,這些當然都讓寫 λ 變得非常容易。
最近的一些工業用語言,像 Go 、 Rust 、 Scala ,也全數是函數程式設計語言。終於到了最近兩年,因為 Facebook 把 PHP 改造成 Hack 語言的關係,連 PHP 自己都不得不採用了 λ ,這還是去年的事情。
接下來, C 的徒子徒孫們, Objective-C 的 block , C++11 以及最近剛出的 Swift ,當然也都是用 λ ,每一個都是函數式程式設計語言。終於到了今年三月,最後一個,大家以為永遠不會淪陷的物件導向語言,叫做 Java 的,也淪陷了。[笑聲]
因為 Project Lambda ,從 Java 8 開始,完全支援 λ ,函數程式設計。所以除了底下那四個,語言界的活化石之外(投影片下方從右到左: Ada 、Cobol 、 ANSI C 、 Fortran ),這年頭所謂「函數式程式設計語言」,就是「程式設計語言」,這兩者完全沒有什麼差別。唯一的差別只是你要用多少 λ、你要怎麼用 λ ,而不是說你的語言支不支援 λ 。除非你很不幸地在用這四個活化石語言,不然你的語言,一定都支援 λ 。
所以我想今天我想講的並不是 λ calculus ,而是這十年來左右,函數式程式語言在這上面再加上去,用 λ 構築出的一些功能。這些功能,可能沒有 closure 那麼普及,但是按照技術輸出的程度,我們可以想像,它們接下來四五年,也會變成主流語言的功能。
第一個,叫做 generator ,如果有寫 Python 的朋友,應該滿熟悉 generator 的。這裡我們可以看到 Haskell 裡面,這裡是個滿經典的範例(投影片 31 到 33 頁),就是我們要定義斐波那契數列的時候,我們可以定義一個無限長的串列,它需要無限多的記憶體,才能夠表示......沒有啦(笑),它需要無限長的記憶體,才能夠全部印出來。
它 的第一項是 1 ,第二項是 1 ,它的第三項開始是 x + y ,其中 x 是從它自己給, y 是從它自己的第二項開始給,所以它的第三項就是 2 = 1 + 1 ,接下來 + 2 = 3 ,所以就 1 1 2 3 5 8 ... 這樣子,沒有問題吧?所以這個串列是個無限長的串列,我們就可以對它做一些操作。好比像說在這裡,我們可以說要把 fibs 這無限多個,每個都先 * 2 ,接下來出來的這個串列,每個再 * 5 ,接下來我要拿前 6 個,然後把它印出來。丟進 Haskell 裡面,它就會很高興地告訴你是 10 10 20 30 50 80 。
在一個沒有 generator 特性的語言裡面,例如像說 C 裡面,在第一步 map (* 2) fibs ,其實它就已經會卡住了。因為一個無限長的串列,你要對它每一個逐項 * 2 ,需要無限久的時間跟無限多的記憶體,這兩個可能都是一些問題。
但是在 Haskell 裡面,這並不是問題,是因為在 Haskell ,我們不需要像在 Python 或別的語言裡面,用 yield 這個語法來定義 generator , Haskell 裡面,任何時候寫出來的任何函數,自動就是 generator ,所有寫的 List ,它自動因為惰性求值的關係,它需要這個值的時候,才會去算它的關係,每次需要拿六個的時候,就會先拿一個,發現是 1 ,再 * 2 * 5 ,然後 1 2 3 5 8 就停了,第七個事實上沒有用到,所以永遠不會去計算。這是 Haskell 一個有別於幾乎所有其他語言的特性。Haskell 的 GHC 這個大家都在用的編譯器,有一個特性就是 list fusion。list fusion 是這樣一個概念,就是當編譯器看到你寫了兩個 map ,連續寫,就是把某個東西的每一項做某個操作,再做另一個操作的時候,它會自動把兩個 map 改寫成一個 map。改過的 map 裡面就是左右兩邊結合起來。這樣我們不需要額外一個中繼的迴圈,而只要一個迴圈-這個迴圈對這個無限串列的每一項都直接乘以二,乘以五。這個在最佳化上面很重要,因為有些編譯器看到這樣就可以知道是乘十的意思之類這樣子。所以就可以用非常少的記憶體做無限多的操作。
...