iThome 網站首載:盤點語言中的函數味
試著將「Functional Programming」這個關鍵字,之後接上Python、Ruby、JavaScript或任何你想要的語言,在搜尋引擎中進行搜尋,可能都能找到文件討論如何實現,藉由盤點語言中的函數式或任何你想要的典範味道,除了能發現相同概念在各語言中的不同實現,進一步掌握這些元素在語言中的使用時機,更能重新塑造你的思考習慣。
- 盤點一級函式實現方式
在我先前專欄〈往數學領域抽象化的函數程式設計〉中介紹過函數式語言的特徵,其中一級函式是不少主流語言中普遍支援的概念,也就是將函式本身視為值,可作為引數或回傳值傳遞,一級函式概念在語言中多半被實現為兩個部份:具名函式與匿名函式。例如Haskell中,
doubleMe x = x + x
是個具名函式,而\x -> x + x
是個匿名函式,你可以傳遞函式,像是double = doubleMe
或double = \x -> x + x
;在Python或JavaScript中的實現亦是類似,以Python為例,def doubleMe(x): return x + x
是具名函式,而lambda x: x + x
是匿名函式,你可以傳遞函式,像是double = doubleMe
或double = lambda x: x + x
。有些語言具有一級函式的實現,不過並非具名與匿名函式都可以直接當作值傳遞。例如在Ruby中定義的方法(外觀上看像是個函式)就不行,而Ruby的區塊(Block)語法可視為接近匿名函式的實現,不過區塊本身並不是物件,必須配合方法一起使用,而且區塊中的程式碼較像個流程片段,區塊中若有
return
,接受區塊的方法執行至該行時就會結束方法呼叫,這與多數語言中實現一級函式時的呼叫方式並不相同,為了彌補這方面的不足,Ruby提供了lambda
方法,lambda {|x| x + x}
或語法蜜糖->(x) {x + x}
的用法看來會較像是匿名函式。一級函式概念的實現當然不會是動態定型語言的專利,靜態定型語言如Scala甚至Java也有其實現方式,對於具名函式能否直接當作值傳遞,Scala與Java的答案都是「否」,匿名函式本身是否以物件存在,Scala的答案為「是」而Java為「否」。在最初定義語法時就將函式當作一等公民(First-class citizen)看待的語言,具名與匿名函式多半都能作為值傳遞,後續才想在既有典範與語法包袱下納入一等函式概念的語言,多半無法達到這點,有趣的部份就在於瞭解既有典範與語法包袱下為何無法實現,這往往會重新加深你對該語言的認識。
舉例來說,JDK8的Lambda,就因為以物件導向為主要典範或有著泛型語法等的包袱,具名函式不能直接當作值傳遞,而匿名函式本身也不是以物件直接存在,必須配合既有的interface語法來定義Lambda目標型態(Target type),這避免了創造新型態系統是其好處,缺點就是目標型態的命名容易令人困惑。Ruby創建者在《松本行弘的程式設計世界》中解釋了區塊為何會有目前設計方式:減少物件的數量,程式碼外觀上像是擴充控制結構,方法只接受一個區塊的簡單設計,也符合了OCaml高階函式程式庫中,九成多的函式只接受一個函式的調查結果。
- 盤點高階函式(方法)
基本上,高階函式是指可接受一級函式作為引數的函式,這類函式多半在內部實現了極為通用的樣版(Template)流程,而在函式名稱上提供更高一層的流程意義。高階函式中最常見的是我先前專欄〈List處理模式〉中談過的
map
、filter
、fold
等,或者是在〈開發者應認識的資料型態及效用函式〉中看到的slice
、zip
等,甚至是〈神秘的Monad不神秘〉中看過的flatMap
,使用這類高階函式,通常表示你對流程要能分而治之(Divide and conquer),有著更高一層的思考方式,而不再僅僅只是使用if
、for
、while
等語法思考著低階處理流程。使用高階處理函式的另一個考量是,程式碼本身是否能展現意圖。舉例來說,JDK8中
numbers.filter(number -> number > 10)
慣例上表示將大於10的數字留下來,不過Ruby認為filter
會被誤解為過濾掉大於10的數字,改用numbers.select {|number| number > 10}
會清楚許多,類似的理由下,Ruby中用了collect
來取代map
,還有著detect
、reject
等高階方法;Python不愛用map
、filter
、reduce
這類函式,認為List comprehension如[number for number in numbers if number > 10]
會清楚許多,不過松本行弘認為不易閱讀,因為與Ruby由左而右的求值順序不一致,目前並不考慮在Ruby中加入這類語法。儘管可讀性如何實現有些爭議之處,以更高層次思考對待流程,是運用高階函式或語法時該有的態度,只是既然不去處理低階處理流程,有時得留意一下高階函式在惰性求值(Lazy evaluation)與平行處理等方面的可能性,才能掌握高階函式或語法的使用時機。Python中
[x * 10 for x in xrange(100)]
是立即求值(Eager evaluation),將[]
改為()
就是惰性求值;Ruby的陣列操作是立即求值,Ruby 2.0之後,可以使用Enumerable
的lazy
方法,對後續操作做惰性求值;JDK8中Collection
得明確使用stream
方法以進行後續管線(Pipeline)操作,在可能情況下亦可進行惰性求值,如果要進一步取得平行處理的可能性,得明確使用parallelStream
方法。- 盤點鞣製與不全套用概念
在《Learn You a Haskell for Great Good!》中提到「不熟悉鞣製(Curring)與不全套用(Partial application)的人們,往往會寫出很多lambda,而實際上大部分都是沒必要的」,其中舉例,與其寫下
map (\\x -> x + 3) [1, 6, 3, 2]
,不如寫下map (+3) [1, 6, 3, 2]
來得清爽。有些語言亦支援這兩種概念,例如Scala,與其寫下List(1, 2, 3).foreach(x => println(x))
,不如寫下List(1, 2, 3).foreach(println)
來得簡潔好讀。實際上不一定要支援鞣製與不全套用這兩種概念,lambda就是匿名函式概念,一個臨時定義的小運算式,可傳遞給高階函式回呼使用,如果你已經有事先定義好的函式,當然也可以傳遞給高階函式,這是重用現有元件以及考量可讀性的概念。例如在JavaScript中,與其寫下
[1, 2, 3].forEach(function(ele, idx, arr) { console.log(ele, idx, arr); })
,不如改用[1, 2, 3].forEach(console.log)
來得清楚。對於具有一級函式及高階函式概念,但具名函式無法直接作為值傳遞的語言來說,也有其因應之道,例如Ruby可以透過
method
方法取得Method
、透過to_proc
方法取得Proc
物件、透過&
將Proc
物件當作引數傳給高階方法,因而你有各種重用既有方法的機會,而不是直接寫下區塊定義,例如與其寫下[1, 2, 3, 4, 5].reduce { |sum, element| sum + element }
,不如寫下[1, 2, 3, 4, 5].reduce(:+)
來得明確。有了匿名函式,不見得你一定得用匿名函式來解決一切,透過現有或自定義適當名稱的函式,在必要時直接參考函式,在重用與可讀性都會高很多。JDK8中的方法參考(Method reference),也是這類概念的實作。與其寫下
IntStream.of(1, 2, 3).forEach(x -> out.println(x))
,不如寫下IntStream.of(1, 2, 3).forEach(out::println)
來得清楚。- 透過盤點特性重新認識語言
有人對JDK8中
Collection
要進行filter
等操作前,要先呼叫stream
方法有些不解,覺得太過麻煩,其實Collection
的stream
相當於Scala的view
,知道嗎?Scala中(1 to 1000000000).view.filter(_ % 2 == 0).take(10).toArray
,沒有那個view
方法,將會有OutOfMemoryError
錯誤,實際上JDK8中對應的程式是rangeClosed(1, 1000000000).filter(it -> it % 2 == 0).limit(10).toArray()
,寫stream
或view
是否太麻煩不是重點,重點是你有沒有惰性求值的概念。不少程式語言看似相互競爭,實際上是相互學習,只是換了個實作樣貌,有時概念是在實作中看來很明確,有時概念是隱含在實作中,盤點語言中的函數味只是個晃子,實際上你可以盤點語言中的各種概念實作,像是物件導向、meta-programming等概念在不同語言中的實現,在盤點的過程中,你會發現語言中過去未曾注意過的考量,對程式的思考方式也會改變,從而在適當場合更加善用語言中適當的元素。