<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    emu in blogjava

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

    牢記:函數式編程不是用函數來編程!!!


    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.slice(0, 1))].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:
    posted on 2007-08-21 11:09 emu 閱讀(1321) 評論(2)  編輯  收藏 所屬分類: DHTML和JAVASCRIPT 技術

    評論

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

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

    主站蜘蛛池模板: 久久不见久久见免费影院www日本 久久WWW免费人成—看片 | 免费毛片在线看片免费丝瓜视频| 亚洲AV无码久久久久网站蜜桃| 四虎成人免费影院网址| 人禽伦免费交视频播放| 97久久精品亚洲中文字幕无码 | 99国产精品视频免费观看| 亚洲精品123区在线观看| 国产a v无码专区亚洲av| 99国产精品免费视频观看| 男人的天堂av亚洲一区2区| 国产AV无码专区亚洲AWWW| 国产片AV片永久免费观看| 免费高清A级毛片在线播放| 亚洲美女自拍视频| 亚洲乱码日产精品a级毛片久久| 免费人成在线观看网站品爱网 | 亚洲欧美日韩一区二区三区| 亚洲色婷婷综合久久| 成人免费看吃奶视频网站| 99久久国产精品免费一区二区| 亚洲色大成网站WWW国产| 久久精品国产亚洲av成人| 香蕉视频在线观看免费国产婷婷| 国产免费无码AV片在线观看不卡| 亚洲久热无码av中文字幕| 99久久亚洲综合精品成人网| 亚洲精品人成无码中文毛片| 国产乱子精品免费视观看片| 久久久WWW成人免费精品| 亚洲成AV人影片在线观看| 亚洲理论片在线中文字幕| 国产亚洲人成网站在线观看| 午夜两性色视频免费网站| 91av视频免费在线观看| 国产特黄一级一片免费| 国产精品亚洲一区二区三区久久| 亚洲电影在线播放| 亚洲AV本道一区二区三区四区| 亚洲第一黄色网址| 国产成人精品免费直播|