emu in blogjava

            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            171 隨筆 :: 103 文章 :: 1052 評論 :: 2 Trackbacks
          JavaScript與函數(shù)式編程
          作者:月影

          牢記:函數(shù)式編程不是用函數(shù)來編程?。?!


          23.4函數(shù)式編程 
          23.4.1 什么是函數(shù)式編程

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

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

                  那么回到這個問題,什么是函數(shù)式編程?答案很長……
                  
          函數(shù)式編程第一定律:函數(shù)是第一型。

                  這句話本身該如何理解?什么才是真正的第一型?我們看下面的數(shù)學(xué)概念:

                  二元方程式 F(x, y) = 0,x, y 是變量, 把它寫成 y = f(x), x是參數(shù),y是返回值,f是由x到y(tǒng)的映射關(guān)系,被稱為函數(shù)。如果又有,G(x, y, z) = 0,或者記為 z = g(x, y),g是x、y到z的映射關(guān)系,也是函數(shù)。如果g的參數(shù)x, y又滿足前面的關(guān)系y = f(x), 那么得到z = g(x, y) = g(x, f(x)),這里有兩重含義,一是f(x)是x上的函數(shù),又是函數(shù)g的參數(shù),二是g是一個比f更高階的函數(shù)。
                  這樣我們就用z = g(x, f(x)) 來表示方程F(x, y) = 0和G(x, y, z) = 0的關(guān)聯(lián)解,它是一個迭代的函數(shù)。我們也可以用另一種形式來表示g,記z = g(x, y, f),這樣我們將函數(shù)g一般化為一個高階函數(shù)。同前面相比,后面這種表示方式的好處是,它是一種更加泛化的模型,例如T(x,y) = 0和G(x,y,z) = 0的關(guān)聯(lián)解,我們也可以用同樣的形式來表示(只要令f=t)。在這種支持把問題的解轉(zhuǎn)換成高階函數(shù)迭代的語言體系中,函數(shù)就被稱為“第一型”。
                  JavaScript中的函數(shù)顯然是“第一型”。下面就是一個典型的例子:
                  
                  Array.prototype.each = function(closure)
                          {
                          return this.length ? [closure(this.slice(0, 1))].concat(this.slice(1).each(closure)) : [];
                          }

          這真是個神奇的魔法代碼,它充分發(fā)揮了函數(shù)式的魅力,在整個代碼中只有函數(shù)(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]。

          函數(shù)式和面向?qū)ο蟮谋举|(zhì)都是“道法自然”。如果說,面向?qū)ο笫且环N真實世界的模擬的話,那么函數(shù)式就是數(shù)學(xué)世界的模擬,從某種意義上說,它的抽象程度比面向?qū)ο蟾撸驗閿?shù)學(xué)系統(tǒng)本來就具有自然界所無法比擬的抽象性。

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

          閉包,在前面的章節(jié)中我們已經(jīng)解釋過了,它對于函數(shù)式編程非常重要。它最大的特點是不需要通過傳遞變量(符號)的方式就可以從內(nèi)層直接訪問外層的環(huán)境,這為多重嵌套下的函數(shù)式程序帶來了極大的便利性,下面是一個例子:

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

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

          什么是Currying? 它是一個有趣的概念。還是從數(shù)學(xué)開始:我們說,考慮一個三維空間方程 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), 我們說函數(shù)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函數(shù),它是當(dāng)x = 2時,關(guān)于參數(shù)y的函數(shù),注意到上面也用到了閉包的特性。

          有趣的是,我們可以給任意函數(shù)一般化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;
          }

          函數(shù)式編程第四定律:延遲求值和延續(xù)。
                  //TODO:這里再考慮下
                  

          23.4.2 函數(shù)式編程的優(yōu)點

          單元測試

          嚴(yán)格函數(shù)式編程的每一個符號都是對直接量或者表達式結(jié)果的引用,沒有函數(shù)產(chǎn)生副作用。因為從未在某個地方修改過值,也沒有函數(shù)修改過在其作用域之外的量并被其他函數(shù)使用(如類成員或全局變量)。這意味著函數(shù)求值的結(jié)果只是其返回值,而惟一影響其返回值的就是函數(shù)的參數(shù)。
          這是單元測試者的夢中仙境(wet dream)。對被測試程序中的每個函數(shù),你只需在意其參數(shù),而不必考慮函數(shù)調(diào)用順序,不用謹(jǐn)慎地設(shè)置外部狀態(tài)。所有要做的就是傳遞代表了邊際情況的參數(shù)。如果程序中的每個函數(shù)都通過了單元測試,你就對這個軟件的質(zhì)量有了相當(dāng)?shù)淖孕?。而命令式編程就不能這樣樂觀了,在 Java 或 C++ 中只檢查函數(shù)的返回值還不夠——我們還必須驗證這個函數(shù)可能修改了的外部狀態(tài)。

          調(diào)試

          如果一個函數(shù)式程序不如你期望地運行,調(diào)試也是輕而易舉。因為函數(shù)式程序的 bug 不依賴于執(zhí)行前與其無關(guān)的代碼路徑,你遇到的問題就總是可以再現(xiàn)。在命令式程序中,bug 時隱時現(xiàn),因為在那里函數(shù)的功能依賴與其他函數(shù)的副作用,你可能會在和 bug 的產(chǎn)生無關(guān)的方向探尋很久,毫無收獲。函數(shù)式程序就不是這樣——如果一個函數(shù)的結(jié)果是錯誤的,那么無論之前你還執(zhí)行過什么,這個函數(shù)總是返回相同的錯誤結(jié)果。
          一旦你將那個問題再現(xiàn)出來,尋其根源將毫不費力,甚至?xí)屇汩_心。中斷那個程序的執(zhí)行然后檢查堆棧,和命令式編程一樣,棧里每一次函數(shù)調(diào)用的參數(shù)都呈現(xiàn)在你眼前。但是在命令式程序中只有這些參數(shù)還不夠,函數(shù)還依賴于成員變量,全局變量和類的狀態(tài)(這反過來也依賴著這許多情況)。函數(shù)式程序里函數(shù)只依賴于它的參數(shù),而那些信息就在你注視的目光下!還有,在命令式程序里,只檢查一個函數(shù)的返回值不能夠讓你確信這個函數(shù)已經(jīng)正常工作了,你還要去查看那個函數(shù)作用域外數(shù)十個對象的狀態(tài)來確認(rèn)。對函數(shù)式程序,你要做的所有事就是查看其返回值!
          沿著堆棧檢查函數(shù)的參數(shù)和返回值,只要發(fā)現(xiàn)一個不盡合理的結(jié)果就進入那個函數(shù)然后一步步跟蹤下去,重復(fù)這一個過程,直到它讓你發(fā)現(xiàn)了 bug 的生成點。

          并行
          函數(shù)式程序無需任何修改即可并行執(zhí)行。不用擔(dān)心死鎖和臨界區(qū),因為你從未用鎖!函數(shù)式程序里沒有任何數(shù)據(jù)被同一線程修改兩次,更不用說兩個不同的線程了。這意味著可以不假思索地簡單增加線程而不會引發(fā)折磨著并行應(yīng)用程序的傳統(tǒng)問題。
          事實既然如此,為什么并不是所有人都在需要高度并行作業(yè)的應(yīng)用中采用函數(shù)式程序?嗯,他們正在這樣做。愛立信公司設(shè)計了一種叫作 Erlang 的函數(shù)式語言并將它使用在需要極高抗錯性和可擴展性的電信交換機上。還有很多人也發(fā)現(xiàn)了 Erlang 的優(yōu)勢并開始使用它。我們談?wù)摰氖请娦磐ㄐ趴刂葡到y(tǒng),這與設(shè)計華爾街的典型系統(tǒng)相比對可靠性和可升級性要求高了得多。實際上,Erlang 系統(tǒng)并不可靠和易擴展,JavaScript 才是。Erlang 系統(tǒng)只是堅如磐石。
          關(guān)于并行的故事還沒有就此停止,即使你的程序本身就是單線程的,那么函數(shù)式程序的編譯器仍然可以優(yōu)化它使其運行于多個CPU上。請看下面這段代碼:

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

          在函數(shù)編程語言中,編譯器會分析代碼,辨認(rèn)出潛在耗時的創(chuàng)建字符串s1和s2的函數(shù),然后并行地運行它們。這在命令式語言中是不可能的,因為在那里,每個函數(shù)都有可能修改了函數(shù)作用域以外的狀態(tài)并且其后續(xù)的函數(shù)又會依賴這些修改。在函數(shù)式語言里,自動分析函數(shù)并找出適合并行執(zhí)行的候選函數(shù)簡單的像自動進行的函數(shù)內(nèi)聯(lián)化!在這個意義上,函數(shù)式風(fēng)格的程序是“不會過時的技術(shù)(future proof)”(即使不喜歡用行業(yè)術(shù)語,但這回要破例一次)。硬件廠商已經(jīng)無法讓CPU運行得更快了,于是他們增加了處理器核心的速度并因并行而獲得了四倍的速度提升。當(dāng)然他們也順便忘記提及我們的多花的錢只是用在了解決平行問題的軟件上了。一小部分的命令式軟件和 100% 的函數(shù)式軟件都可以直接并行運行于這些機器上。

          代碼熱部署

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

          機器輔助的推理和優(yōu)化

          函數(shù)式語言的一個有趣的屬性就是他們可以用數(shù)學(xué)方式推理。因為一種函數(shù)式語言只是一個形式系統(tǒng)的實現(xiàn),所有在紙上完成的運算都可以應(yīng)用于用這種語言書寫的程序。編譯器可以用數(shù)學(xué)理論將轉(zhuǎn)換一段代碼轉(zhuǎn)換為等價的但卻更高效的代碼[7]。多年來關(guān)系數(shù)據(jù)庫一直在進行著這類優(yōu)化。沒有理由不能把這一技術(shù)應(yīng)用到常規(guī)軟件上。
          另外,還能使用這些技術(shù)來證明部分程序的正確,甚至可能創(chuàng)建工具來分析代碼并為單元測試自動生成邊界用例!對穩(wěn)固的系統(tǒng)這種功能沒有價值,但如果你要設(shè)計心房脈沖產(chǎn)生器 (pace maker)或空中交通控制系統(tǒng),這種工具就不可或缺。如果你編寫的應(yīng)用程序不是產(chǎn)業(yè)的核心任務(wù),這類工具也是你強于競爭對手的殺手锏。

          23.4.3 函數(shù)式編程的缺點

          閉包的副作用

                  非嚴(yán)格函數(shù)式編程中,閉包可以改寫外部環(huán)境(在上一章中我們已經(jīng)見過了),這帶來了副作用,當(dāng)這種副作用頻繁出現(xiàn)并經(jīng)常改變程序運行環(huán)境時,錯誤就變得難以跟蹤。
                  //TODO:

          遞歸的形式
                  
                  盡管遞歸通常是一種最簡潔的表達形式,但它確實不如非遞歸的循環(huán)來的直觀。
                  //TODO:

          延遲取值的弱點

                  //TODO:
          posted on 2007-08-21 11:09 emu 閱讀(1322) 評論(2)  編輯  收藏 所屬分類: DHTML和JAVASCRIPT 技術(shù)

          評論

          # re: 轉(zhuǎn)一篇月影未完成的精品文章:JavaScript與函數(shù)式編程 2008-02-22 13:05 馬猴
          非常不錯,感觸頗深!想寫出簡單而優(yōu)雅的代碼,就得用函數(shù)式的編程。  回復(fù)  更多評論
            

          # re: 轉(zhuǎn)一篇月影未完成的精品文章:JavaScript與函數(shù)式編程 2008-03-28 13:57 dripstone
          接觸js已經(jīng)有將近一年了,認(rèn)為自己掌握了js的基本東西,看到你的blog讓我知道我所見到的只不過是九牛一毛而已,很高興能看到你這么多關(guān)于js的文章,收獲不小,希望以后有機會可以跟你多多請教  回復(fù)  更多評論
            

          主站蜘蛛池模板: 黄平县| 镇雄县| 宁津县| 正阳县| 德惠市| 福贡县| 高清| 闵行区| 渝中区| 贵南县| 威远县| 盐津县| 常德市| 东阿县| 海林市| 霸州市| 揭东县| 冀州市| 南宫市| 沐川县| 海淀区| 涪陵区| 涞源县| 博罗县| 桃江县| 青龙| 福安市| 平昌县| 灵武市| 河西区| 漳平市| 封开县| 临武县| 麻栗坡县| 伊金霍洛旗| 霍林郭勒市| 宁波市| 溧水县| 铜鼓县| 三门县| 绥中县|