今天偶然發現公司裏有同事發佈了一個函數式深度學習框架,恰好最近我也在入門深度學習,因此去了解了一下函數式編程在深度學習裏面的應用。在查資料時我找到了今天要翻譯的這篇文章。算法
這篇文章做者是 Joel Spolsky。他是 Trello 的聯合創始人,Stack Overflow 的聯合創始人和現任 CEO。編程
某天,你在瀏覽你寫的代碼時發現了兩塊代碼幾乎長得如出一轍。好比:api
alert("I'd like some Spaghetti!")
alert("I'd like some Chocolate Moose!")
複製代碼
這兩行代碼惟一的不一樣就是 'Spaghetti' 和 'Chocolate Moose'。他們是用 JavaScript 寫的,可是你不用懂 JS 也能知道這些代碼在幹嗎。這兩行代碼固然看起來不對勁,你能夠建立一個函數來優化下:數組
function SwedishChef(food) {
alert("I'd like some " + food + '!')
}
SwedishChef('Spaghetti')
SwedishChef('Chocolate Moose')
複製代碼
這個例子太過於簡單了,不過你能夠擴展想象,當代碼過於複雜時,這種寫法能帶來的好處。你可能已經知道這些好處了,好比易讀,易維護。抽象就是好!服務器
而後你又發現有兩塊代碼幾乎長得如出一轍,區別就是一塊代碼反覆調用一個叫 BoomBoom
的函數,而另外一塊代碼反覆調用一個叫 PutInPot
的函數:框架
alert('get the lobster')
PutInPot('lobster')
PutInPot('water')
alert('get the chicken')
BoomBoom('chicken')
BoomBoom('coconut')
複製代碼
如今你須要把一個函數傳給另外一個函數來讓上面的代碼好看點。函數接受函數爲參數是編程語言的一個很重要的能力,它能幫你把代碼中重複的部分抽離到一個函數中去:編程語言
function Cook(i1, i2, f) {
alert('get the ' + i1)
f(i1)
f(i2)
}
Cook('lobster', 'water', PutInPot)
Cook('chicken', 'coconut', BoomBoom)
複製代碼
看!咱們把函數做爲參數傳給另外一個函數。分佈式
你的編程語言能作到嗎?函數式編程
等等……假設你尚未定義 PutInPot
和 BoomBoom
,咱們要是能直接把這兩個函數行內傳入,而不是先在別處定義這兩個函數,不是很棒嗎?像這樣:函數
Cook('lobster', 'water', function(x) {
alert('pot ' + x)
})
Cook('chicken', 'coconut', function(x) {
alert('boom ' + x)
})
複製代碼
這真是太方便了。我隨意寫個函數就塞給另外一個函數,都不用給入參函數命名。
一旦你開始思考把匿名函數當作參數傳遞,你可能會意識處處處可見的某種代碼,好比,對數組的每個元素進行操做:
var a = [1, 2, 3]
for (i = 0; i < a.length; i++) {
a[i] = a[i] * 2
}
for (i = 0; i < a.length; i++) {
alert(a[i])
}
複製代碼
操做數組的每一個元素是個很經常使用的操做,你能夠寫個函數來幫你幹這事:
function map(fn, a) {
for (i = 0; i < a.length; i++) {
a[i] = fn(a[i])
}
}
複製代碼
而後你能夠這樣重構上面的數組操做代碼:
map(function(x) {
return x * 2
}, a)
map(alert, a)
複製代碼
另外一個經常使用的數組操做是把數組的每一個元素按某種方式鏈接起來:
function sum(a) {
var s = 0
for (i = 0; i < a.length; i++) s += a[i]
return s
}
function join(a) {
var s = ''
for (i = 0; i < a.length; i++) s += a[i]
return s
}
alert(sum([1, 2, 3]))
alert(join(['a', 'b', 'c']))
複製代碼
sum
和 join
長得太像了,你可能想把它們的本質部分(把一個數組的全部元素按某種方式鏈接成一個值)抽象到一個通用函數裏面去:
function reduce(fn, a, init) {
var s = init
for (i = 0; i < a.length; i++) s = fn(s, a[i])
return s
}
function sum(a) {
return reduce(
function(a, b) {
return a + b
},
a,
0
)
}
function join(a) {
return reduce(
function(a, b) {
return a + b
},
a,
''
)
}
複製代碼
不少老的編程語言根本就沒辦法作到上面展現的這些程序抽象。另一些語言容許你這樣幹,可是很難作到(例如,C 語言有函數指針,可是你必須把函數聲明和定義在其它地方)。面向對象編程語言沒有被徹底說服,開發者應該用函數來作任何事情。
Java 要求你先建立一個叫函子的帶有單一方法的完整對象,而後才能把函數當作一等對象。(譯者注:原文發表於 2006 年,當時 Java 8 尚未發佈,lambda 表達式在 Java 中還不存在)。另外,不少面嚮對象語言要求你爲每個類建立一個文件,很快你的代碼就變得笨拙臃腫。若是你的編程語言要求你寫個函子才能實現函數一等對象,你就沒有獲得現代編程環境帶來的一些好處。
就寫個能幫你遍歷數組的每一個元素的函數而已,能給你帶來什麼好處?
咱們仍是回到前面提到的 map
函數。當你須要對數組裏面的每一個元素作某種操做時,這些操做的順序可能並不重要。那麼,假如你有兩個 CPU,那你就能夠寫段代碼讓每一個 CPU 計算一半的數組,這樣 map
運行速度就兩倍快了。
再假如,你有分佈在全球各個數據中心的幾十萬個服務器,而後你有一個超級大數組,這個大數組包含了整個互聯網的內容。那如今你就能夠在這幾十萬臺計算機上運行 map
函數了,每臺計算機解決數組的一小塊部分。
如今,搜索整個互聯網的內容就簡單到執行一個 map
函數,並給 map
傳一個查詢字符串就好了。
我但願你注意到的真正有趣的事情是,一旦你意識到 map
和 reduce
函數是每一個開發者都能用的,你就只須要找個超級天才幫你寫段比較難寫的代碼,讓 map
和 reduce
運行在一個大型的並行計算機集羣上。而後你實現的這種分佈式計算會比以前用 for 循環寫的一次完成全部任務的老代碼快無數倍。
讓我再重複一遍。把循環這個概念從你的代碼中抽象出去以後,你能夠用任何方式來實現循環,包括用上面提到的可利用多餘硬件來靈活伸縮的分佈式計算。
如今,你應該明白了我爲何以前會抱怨如今的 CS 專業學生只學 Java:
若是你不懂函數式編程,你是不可能發明出 MapReduce 的(谷歌的高可伸縮搜索算法 )。Map 和 Reduce 這兩個術語源自 Lisp 和函數式編程。若是你在學習 CS 6.001 時就學到了純函數程序因爲沒有反作用,因此能夠很簡單完成並行計算,你是很容易理解 MapReduce 的(譯者注:做者在抱怨如今 CS 教育缺失了函數式編程)。谷歌發明出了 MapReduce,而微軟沒有,說明了爲何微軟如今還在試圖弄出可行的搜索算法來遇上谷歌;與此同時,谷歌已經開始研發 Skynet 這個世界最大的並行超級計算機去解決下一個問題了。我認爲微軟還沒搞清楚他們落後了谷歌多少。
(譯者注:谷歌把 MapReduce 算法的論文開放了,你能在這裏讀到)
好啦,如今我但願我已經說服了你,爲何支持一等函數的編程語言能讓你找到更多程序抽象的機會,這意味着你的代碼會變得更輕量,更緊湊,更易複用,和更可伸縮。不少谷歌的應用都用到了 MapReduce 算法。當有人優化 MapReduce 或者修復它的某些 bug 的時候,全部這些應用都受益。
如今我要變得感性一點了。我認爲最有生產力的編程環境必須是那些容許你建立不一樣層級的抽象的語言。老而難用的 FORTRAN 根本不讓你寫函數。C 有函數指針,但你必須把函數聲明和定義在其它地方,這樣寫太醜陋了。Java 逼着你用函子,更醜陋。(見前譯註)
原文糾錯:
上次我使用 FORTRAN 仍是 27 年前了。很明顯它是支持函數的。我寫到這裏的時候確定想到的是 GW-BASIC。
JavaScript 是我入門編程的第一門語言,也是我目前掌握最熟練的語言。我一開始覺得一等公民函數就是一個很普通的特徵,其它語言應該也有,但直到最近我才知道它來自 Lisp,在主流編程語言裏面還比較小衆,目前只有一部分編程語言纔在最近加入 lambda 表達式(我知道的只有 Java 和 Python)。
我不明白爲何有那麼多開發者認爲 JavaScript 垃圾。Lexical scoping(我不知道這個術語對應的中文翻譯是什麼)和一等公民函數的語言特性已經足夠讓你寫出強大而複雜的應用,而這些強大的特性並非全部主流語言都支持的,JS 怎麼就垃圾了?
一樣我也不明白爲何有人認爲 JavaScript 不適合函數式編程。這段時間比較火的 「計算機之子」 對 JS 有這樣的評價:
用 JS 作函數式編程並不靠譜,Map/Reduce/Redux/Hooks 等並非函數式編程,只是長得像而已。
Hooks 借鑑了 Algebraic Effect,有些 FP 的影子,但太雜糅了。Redux 是直接從 Elm 借鑑過來的,不知怎麼就不是函數式。而 map 和 reduce 剛剛已經說得很清楚了,是函數式編程的核心概念,原理上也和 Lisp 一致,怎麼就不函數式了?
JavaScript 是支持多個編程範式的。Vue 的成功已經證實了 JS 在面向對象程序設計上的潛力,但這並無證實 JS 不適合寫函數式代碼。React 比較函數式,但爲了照顧開發者的接受程度,作出太多妥協,它本能夠更函數式。
用時下開發者的接受程度來判斷一個編程語言是否具備某些特性顯然是荒唐的。真這樣的話,React 不會探索出這麼多新的可能。若是你理解你在幹什麼,你只須要 JS 提供給你的一些核心能力就實現程序功能。你並不須要使用 Proxy(也不須要 defineProperties), generators, iterators 等新功能,你甚至都不須要原型鏈繼承。而一等公民函數,是提供給你這些能力的核心特性。(我只是說理論上你不須要,沒有鼓勵你和整個開發生態爲敵)