JavaScript與函數式編程解釋

來源:互聯網
上載者:User

作者:月影
牢記:函數式編程不是用函數來編程!!!
23.4函數式編程
23.4.1 什麼是函數式編程

什麼是函數式編程?如果你這麼直白地詢問,會發現它竟是一個不太容易解釋的概念。許多在程式設計領域有著多年經驗的老手,也無法很明白地說清楚函數式編程到底在研究些什麼。函數式編程對於熟悉過程式程式設計的程式員來說的確是一個陌生的領域,閉包(closure),延續(continuation),和柯裡化(currying)這些概念看起來是這麼的陌生,同我們熟悉的if、else、while沒有任何的相似之處。儘管函數式編程有著過程式無法比擬的優美的數學原型,但它又是那麼的高深莫測,似乎只有拿著博士學位的人才玩得轉它。

提示:這一節有點難,但它並不是掌握JavaScript所必需的技能,如果你不想用JavaScript來完成那些用Lisp來完成活兒,或者不想學函數式編程這種深奧的技巧,你完全可以跳過它們,進入下一章的旅程。

那麼回到這個問題,什麼是函數式編程?答案很長……

函數式編程第一定律:函數是第一型。

這句話本身該如何理解?什麼才是真正的第一型?我們看下面的數學概念:

二元方程式 F(x, y) = 0,x, y 是變數, 把它寫成 y = f(x), x是參數,y是傳回值,f是由x到y的映射關係,被稱為函數。如果又有,G(x, y, z) = 0,或者記為 z = g(x, y),g是x、y到z的映射關係,也是函數。如果g的參數x, y又滿足前面的關係y = f(x), 那麼得到z = g(x, y) = g(x, f(x)),這裡有兩重含義,一是f(x)是x上的函數,又是函數g的參數,二是g是一個比f更高階的函數。
這樣我們就用z = g(x, f(x)) 來表示方程F(x, y) = 0和G(x, y, z) = 0的關聯解,它是一個迭代的函數。我們也可以用另一種形式來表示g,記z = g(x, y, f),這樣我們將函數g一般化為一個高階函數。同前面相比,後面這種表示方式的好處是,它是一種更加泛化的模型,例如T(x,y) = 0和G(x,y,z) = 0的關聯解,我們也可以用同樣的形式來表示(只要令f=t)。在這種支援把問題的解轉換成高階函數迭代的語言體系中,函數就被稱為“第一型”。
JavaScript中的函數顯然是“第一型”。下面就是一個典型的例子:

Array.prototype.each = function(closure)
{
return this.length ? [closure(this[0])].concat(this.slice(1).each(closure)) : [];
}

這真是個神奇的魔法代碼,它充分發揮了函數式的魅力,在整個代碼中只有函數(function)和符號(Symbol)。它形式簡潔並且威力無窮。
[1,2,3,4].each(function(x){return x * 2})得到[2,4,6,8],而[1,2,3,4].each(function(x){return x-1})得到[0,1,2,3]。

函數式和物件導向的本質都是“道法自然”。如果說,物件導向是一種真實世界的類比的話,那麼函數式就是數學世界的類比,從某種意義上說,它的抽象程度比物件導向更高,因為數學系統本來就具有自然界所無法比擬的抽象性。

函數式編程第二定律:閉包是函數式編程的摯友。

閉包,在前面的章節中我們已經解釋過了,它對於函數式編程非常重要。它最大的特點是不需要通過傳遞變數(符號)的方式就可以從內層直接存取外層的環境,這為多重嵌套下的函數式程式帶來了極大的便利性,下面是一個例子:

(function outerFun(x)
{
return function innerFun(y)
{
return x * y;
}
})(2)(3);

函數式編程第三定律:函數可以被科裡化(Currying)。

什麼是Currying? 它是一個有趣的概念。還是從數學開始:我們說,考慮一個三維空間方程 F(x, y, z) = 0,如果我們限定z = 0,於是得到 F(x, y, 0) = 0 記為 F'(x, y)。這裡F'顯然是一個新的方程式,它代表三維空間曲線F(x, y, z)在z = 0平面上的兩維投影。記y = f(x, z), 令z = 0, 得到 y = f(x, 0),記為 y = f'(x), 我們說函數f'是f的一個Currying解。
下面給出了JavaScript的Currying的例子:
function add(x, y)
{
if(x!=null && y!=null) return x + y;
else if(x!=null && y==null) return function(y)
{
return x + y;
}
else if(x==null && y!=null) return function(x)
{
return x + y;
}
}
var a = add(3, 4);
var b = add(2);
var c = b(10);

上面的例子中,b=add(2)得到的是一個add()的Currying函數,它是當x = 2時,關於參數y的函數,注意到上面也用到了閉包的特性。

有趣的是,我們可以給任意函數一般化Currying,例如:

function Foo(x, y, z, w)
{
var args = arguments;

if(Foo.length < args.length)
return function()
{
return
args.callee.apply(Array.apply([], args).concat(Array.apply([], arguments)));
}
else
return x + y – z * w;
}

函數式編程第四定律:延遲求值和延續。
//TODO:這裡再考慮下

23.4.2 函數式編程的優點

單元測試

嚴格函數式編程的每一個符號都是對直接量或者運算式結果的引用,沒有函數產生副作用。因為從未在某個地方修改過值,也沒有函數修改過在其範圍之外的量並被其他函數使用(如類成員或全域變數)。這意味著函數求值的結果只是其傳回值,而惟一影響其傳回值的就是函數的參數。
這是單元測試者的夢中仙境(wet dream)。對被測試程式中的每個函數,你只需在意其參數,而不必考慮函數調用順序,不用謹慎地設定外部狀態。所有要做的就是傳遞代表了邊際情況的參數。如果程式中的每個函數都通過了單元測試,你就對這個軟體的品質有了相當的自信。而命令式編程就不能這樣樂觀了,在 Java 或 C++ 中只檢查函數的傳回值還不夠——我們還必須驗證這個函數可能修改了的外部狀態。

調試

如果一個函數式程式不如你期望地運行,調試也是輕而易舉。因為函數式程式的 bug 不依賴於執行前與其無關的代碼路徑,你遇到的問題就總是可以再現。在命令式程式中,bug 時隱時現,因為在那裡函數的功能依賴與其他函數的副作用,你可能會在和 bug 的產生無關的方向探尋很久,毫無收穫。函數式程式就不是這樣——如果一個函數的結果是錯誤的,那麼無論之前你還執行過什麼,這個函數總是返回相同的錯誤結果。
一旦你將那個問題再現出來,尋其根源將毫不費力,甚至會讓你開心。中斷那個程式的執行然後檢查堆棧,和命令式編程一樣,棧裡每一次函數調用的參數都呈現在你眼前。但是在命令式程式中只有這些參數還不夠,函數還依賴於成員變數,全域變數和類的狀態(這反過來也依賴著這許多情況)。函數式程式裡函數只依賴於它的參數,而那些資訊就在你注視的目光下!還有,在命令式程式裡,只檢查一個函數的傳回值不能夠讓你確信這個函數已經正常工作了,你還要去查看那個函數範圍外數十個對象的狀態來確認。對函數式程式,你要做的所有事就是查看其傳回值!
沿著堆棧檢查函數的參數和傳回值,只要發現一個不盡合理的結果就進入那個函數然後一步步跟蹤下去,重複這一個過程,直到它讓你發現了 bug 的產生點。

並行
函數式程式無需任何修改即可並存執行。不用擔心死結和臨界區,因為你從未用鎖!函數式程式裡沒有任何資料被同一線程修改兩次,更不用說兩個不同的線程了。這意味著可以不假思索地簡單增加線程而不會引發折磨著並行應用程式的傳統問題。
事實既然如此,為什麼並不是所有人都在需要高度並行作業的應用中採用函數式程式?嗯,他們正在這樣做。愛立信公司設計了一種叫作 Erlang 的函數式語言並將它使用在需要極高抗錯性和可擴充性的電信交換器上。還有很多人也發現了 Erlang 的優勢並開始使用它。我們談論的是電信通訊控制系統,這與設計華爾街的典型系統相比對可靠性和可升級性要求高了得多。實際上,Erlang 系統並不可靠和易擴充,JavaScript 才是。Erlang 系統只是堅如磐石。
關於並行的故事還沒有就此停止,即使你的程式本身就是單線程的,那麼函數式程式的編譯器仍然可以最佳化它使其運行於多個CPU上。請看下面這段代碼:

String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);

在函數程式設計語言中,編譯器會分析代碼,辨認出潛在耗時的建立字串s1和s2的函數,然後並行地運行它們。這在命令式語言中是不可能的,因為在那裡,每個函數都有可能修改了函數範圍以外的狀態並且其後續的函數又會依賴這些修改。在函數式語言裡,自動分析函數並找出適合并行執行的候選函數簡單的像自動進行的函數內聯化!在這個意義上,函數式風格的程式是“不會過時的技術(future proof)”(即使不喜歡用行業術語,但這回要破例一次)。硬體廠商已經無法讓CPU運行得更快了,於是他們增加了處理器核心的速度並因並行而獲得了四倍的速度提升。當然他們也順便忘記提及我們的多花的錢只是用在瞭解決平行問題的軟體上了。一小部分的命令式軟體和 100% 的函數式軟體都可以直接並行運行於這些機器上。

代碼熱部署

過去要在 Windows上安裝更新,重啟電腦是難免的,而且還不只一次,即使是安裝了一個新版的媒體播放器。Windows XP 大大改進了這一狀態,但仍不理想(我今天工作時運行了Windows Update,現在一個煩人的表徵圖總是顯示在托盤裡除非我重啟一次機器)。Unix系統一直以來以更好的模式運行,安裝更新時只需停止系統相關的組件,而不是整個作業系統。即使如此,對一個大規模的伺服器應用這還是不能令人滿意的。電信系統必須100%的時間運行,因為如果在系統更新時緊急撥號失效,就可能造成生命的損失。華爾街的公司也沒有理由必須在周末停止服務以安裝更新。
理想的情況是完全不停止系統任何組件來更新相關的代碼。在命令式的世界裡這是不可能的。考慮運行時上傳一個Java類並重載一個新的定義,那麼所有這個類的執行個體都將不可用,因為它們被儲存的狀態丟失了。我們可以著手寫些繁瑣的版本控制碼來解決這個問題,然後將這個類的所有執行個體序列化,再銷毀這些執行個體,繼而用這個類新的定義來重新建立這些執行個體,然後載入先前被序列化的資料並希望載入代碼可以恰到地將這些資料移植到新的執行個體。在此之上,每次更新都要重新手動編寫這些用來移植的代碼,而且要相當謹慎地防止破壞對象間的相互關係。理論簡單,但實踐可不容易。
對函數式的程式,所有的狀態即傳遞給函數的參數都被儲存在了堆棧上,這使的熱部署輕而易舉!實際上,所有我們需要做的就是對工作中的代碼和新版本的代碼做一個差異比較,然後部署新代碼。其他的工作將由一個語言工具自動完成!如果你認為這是個科幻故事,請再思考一下。多年來 Erlang工程師一直更新著他們的運轉著的系統,而無需中斷它。

機器輔助的推理和最佳化

函數式語言的一個有趣的屬性就是他們可以用數學方式推理。因為一種函數式語言只是一個形式系統的實現,所有在紙上完成的運算都可以應用於用這種語言書寫的程式。編譯器可以用數學理論將轉換一段代碼轉換為等價的但卻更高效的代碼[7]。多年來關聯式資料庫一直在進行著這類最佳化。沒有理由不能把這一技術應用到常規軟體上。
另外,還能使用這些技術來證明部分程式的正確,甚至可能建立工具來分析代碼並為單元測試自動產生邊界用例!對穩固的系統這種功能沒有價值,但如果你要設計心房脈衝產生器 (pace maker)或空中交通控制系統,這種工具就不可或缺。如果你編寫的應用程式不是產業的核心任務,這類工具也是你強於競爭者的殺手鐧。

23.4.3 函數式編程的缺點

閉包的副作用

非嚴格函數式編程中,閉包可以改寫外部環境(在上一章中我們已經見過了),這帶來了副作用,當這種副作用頻繁出現並經常改變程式運行環境時,錯誤就變得難以跟蹤。
//TODO:

遞迴的形式

儘管遞迴通常是一種最簡潔的表達形式,但它確實不如非遞迴的迴圈來的直觀。
//TODO:

延遲取值的弱點

//TODO:

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.