從函數(shù)式角度看面向?qū)ο笕筇匦匀秉c(iOS篇)

前言

我不知道你都收藏了些什么们颜,我的閱讀清單里面相當(dāng)大部分都是函數(shù)式編程相關(guān)的東東:基本上是最難啃的。這些文章充斥著無比枯燥的教科書語言斯棒,我想就連那些在華爾街浸淫10年以上的大牛都無法搞懂這些函數(shù)式編程(簡稱FP)。你可以去花旗集團或者德意志銀行找個項目經(jīng)理來問問1:你們?yōu)槭裁匆xJMS而不用Erlang是越?答案基本上是:我認(rèn)為這個學(xué)術(shù)用的語言還無法勝任實際應(yīng)用。可是,現(xiàn)有的一些系統(tǒng)不僅非常復(fù)雜還需要滿足十分嚴(yán)苛的需求薪贫,它們就都是用函數(shù)式編程的方法來實現(xiàn)的。這刻恭,就說不過去了瞧省。

關(guān)于FP的文章確實比較難懂,但我不認(rèn)為一定要搞得那么晦澀。有一些歷史原因造成了這種知識斷層鞍匾,可是FP概念本身并不難理解交洗。我希望這篇文章可以成為一個“FP入門指南”,幫助你從指令式編程走向函數(shù)式編程橡淑。先來點咖啡构拳,然后繼續(xù)讀下去。很快你對FP的理解就會讓同事們刮目相看了梳码。

什么是函數(shù)式編程(Functional Programming隐圾,F(xiàn)P)伍掀?它從何而來掰茶?可以吃嗎?倘若它真的像那些鼓吹FP的人說的那么好蜜笤,為什么實際應(yīng)用中那么少見濒蒋?為什么只有那些在讀博士的家伙想要用它?而最重要的是把兔,它母親的怎么就那么難學(xué)沪伙?那些所謂的closure、continuation县好,currying围橡,lazy evaluation還有no side effects都是什么東東(譯者:本著保留專用術(shù)語的原則,此處及下文類似情形均不譯)缕贡?如果沒有那些大學(xué)教授的幫忙怎樣把它應(yīng)用到實際工程里去翁授?為什么它和我們熟悉的萬能而神圣的指令式編程那么的不一樣?

函數(shù)式概念

又稱泛函編程晾咪,是一種編程范型收擦,它將電腦運算視為數(shù)學(xué)上的函數(shù)計算,并且避免使用程序狀態(tài)以及易變對象谍倦。函數(shù)編程語言最重要的基礎(chǔ)是λ演算(lambda calculus)塞赂。而且λ演算的函數(shù)可以接受函數(shù)當(dāng)作輸入(引數(shù))和輸出(傳出值)。

比起命令式編程昼蛀,函數(shù)式編程更加強調(diào)程序執(zhí)行的結(jié)果而非執(zhí)行的過程宴猾,倡導(dǎo)利用若干簡單的執(zhí)行單元讓計算結(jié)果不斷漸進,逐層推導(dǎo)復(fù)雜的運算叼旋,而不是設(shè)計一個復(fù)雜的執(zhí)行過程鳍置。結(jié)果比過程更重要送淆。

函數(shù)式編程中的函數(shù)這個術(shù)語不是指計算機中的函數(shù)(實際上是Subroutine)税产,而是指數(shù)學(xué)中的函數(shù),即自變量的映射。也就是說一個函數(shù)的值僅決定于函數(shù)參數(shù)的值辟拷,不依賴其他狀態(tài)撞羽。比如sqrt(x)函數(shù)計算x的平方根,只要x不變衫冻,不論什么時候調(diào)用诀紊,調(diào)用幾次,值都是不變的隅俘。函數(shù)式編程為我們提供了另外一種抽象和思考的方式邻奠。函數(shù)式編程是一種風(fēng)格與編程語言無關(guān), 面向?qū)ο笠彩且环N風(fēng)格與編程語言無關(guān)为居,兩種風(fēng)格并不矛盾碌宴,可以結(jié)合的- 叫 functional object(Objects in OCaml)

函數(shù)式特性

  • 閉包

    又稱詞法閉包(Lexical Closure)、函數(shù)閉包(function closures)或者lambdas表達式蒙畴,是引用了自由變量的函數(shù)贰镣。這個被引用的自由變量將和這個函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外膳凝。所以碑隆,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。

    對應(yīng)于OC語言中的Blocks蹬音。

    閉包可以理解為匿名函數(shù)上煤,后面的示例將大量使用閉包,閱讀代碼時著淆,請把Block當(dāng)作函數(shù)。

    因為閉包只有在被調(diào)用時才執(zhí)行操作嘉熊,即“惰性求值”孕惜,所以它可以被用來定義控制結(jié)構(gòu)瓮栗。

    ?

  • 惰性求值

    執(zhí)行順序不依賴語句順序进陡,更易并發(fā)糙麦。

    函數(shù)式編程語言還提供惰性求值(Lazy evaluation仆邓,也稱作call-by-need)榜聂,是在將表達式賦值給變量(或稱作綁定)時并不計算表達式的值豌汇,而在變量第一次被使用時才進行計算逻澳。這樣就可以通過避免不必要的求值提升性能瓤逼。

    具體理解,參考閉包。

    ?

  • 函數(shù)是"第一等公民"

    所謂"第一等公民"(first class),指的是函數(shù)與其他數(shù)據(jù)類型一樣沟绪,處于平等地位辈毯,可以賦值給其他變量坝疼,也可以作為參數(shù)钝凶,傳入另一個函數(shù)锌介,或者作為別的函數(shù)的返回值。

    比較容易理解,不舉例說明。

    ?

  • 高階函數(shù)

    高階函數(shù)就是參數(shù)為函數(shù)或返回值為函數(shù)的函數(shù)∪辏現(xiàn)象上就是函數(shù)傳進傳出慢哈,就像面向?qū)ο髮ο鬂M天飛一樣兰绣。有了高階函數(shù),就可以將復(fù)用的粒度降低到函數(shù)級別方妖,相對于面向?qū)ο笳Z言狭魂,復(fù)用的粒度更低党觅。

    這時候我們關(guān)注函數(shù)函數(shù)之間的關(guān)系。

    舉例來說斋泄,假設(shè)有如下的三個函數(shù)魁莉,

    // 例子1:
    typedef NSInteger(^BlockFun)(NSInteger);
    
    BlockFun retSelf = ^(NSInteger a){ return a;};
    BlockFun square = ^(NSInteger a){ return a*a;};
    BlockFun cube = ^(NSInteger a){ return a*a*a;};
    
    NSInteger sumInt(NSInteger a, NSInteger b){
        if (a > b) return 0;
        return (a + sumInt(a + 1, b));
    }
    
    NSInteger sumSquare(NSInteger a, NSInteger b){
        if (a > b) return 0;
        return (square(a) + sumSquare(a + 1, b));
    }
    
    NSInteger sumCube(NSInteger a, NSInteger b){
        if (a > b) return 0;
        return (cube(a) + sumCube(a + 1, b));
    }
    

    分別是求a到b之間整數(shù)之和,求a到b之間整數(shù)的平方和募胃,求a到b之間整數(shù)的立方和旗唁。

    三個函數(shù)不同的只是其中的fun不同,那么是否可以抽象出一個共同的模式呢痹束?

    我們可以定義一個高階函數(shù)sumWithFun:

    NSInteger sumWithFun(BlockFun fun , NSInteger a, NSInteger b){
        if (a > b) return 0;
        return (fun(a) + sumWithFun(fun, a + 1, b));
    }
    

    其中參數(shù)fun是一個函數(shù)检疫,在函數(shù)中調(diào)用fun函數(shù)進行計算,并進行求和祷嘶。

    然后例子1就可以簡化如下:

    // 例子1:
    typedef NSInteger(^BlockFun)(NSInteger);
    
    BlockFun retSelf = ^(NSInteger a){ return a;};
    BlockFun square = ^(NSInteger a){ return a*a;};
    BlockFun cube = ^(NSInteger a){ return a*a*a;};
    NSInteger sumWithFun(BlockFun fun , NSInteger a, NSInteger b){
        if (a > b) return 0;
        return (fun(a) + sumWithFun(fun, a + 1, b));
    }
    // 調(diào)用方式
    NSInteger a = 10, b = 20;
    NSLog(@"%ld, %ld, %ld",
          sumWithFun(retSelf, a, b),
          sumWithFun(square, a, b),
          sumWithFun(cube, a, b)
          );
    

    這樣就可以重用sumWithFun函數(shù)來實現(xiàn)三個函數(shù)中的求和邏輯屎媳。
    (示例來源:https://d396qusza40orc.cloudfront.net/progfun/lecture_slides/week2-2.pdf

    高階函數(shù)提供了一種函數(shù)級別上的依賴注入(或反轉(zhuǎn)控制)機制夺溢,在上面的例子里,sumWithFun函數(shù)的邏輯依賴于注入進來的函數(shù)的邏輯烛谊。很多GoF設(shè)計模式都可以用高階函數(shù)來實現(xiàn)风响,如Visitor,Strategy丹禀,Decorator等状勤。比如Visitor模式就可以用集合類的map()或foreach()高階函數(shù)來替代。

    ?

  • Continuation

    我們對函數(shù)的理解只有一半是正確的双泪,因為這樣的理解基于一個錯誤的假設(shè):函數(shù)一定要把其返回值返回給調(diào)用者荧降。按照這樣的理解,continuation就是更加廣義的函數(shù)攒读。這里的函數(shù)不一定要把返回值傳回給調(diào)用者朵诫,相反,它可以把返回值傳給程序中的任意代碼薄扁。continuation就是一種特別的參數(shù)剪返,把這種參數(shù)傳到函數(shù)中,函數(shù)就能夠根據(jù)continuation將返回值傳遞到程序中的某段代碼中邓梅。說得很高深脱盲,實際上沒那么復(fù)雜。直接來看看下面的例子好了:

    int i = add(5, 10);
    int j = square(i);
    

    add這個函數(shù)將返回15然后這個值會賦給i日缨,這也是add被調(diào)用的地方钱反。接下來i的值又會被用于調(diào)用square。請注意支持惰性求值的編譯器是不能打亂這段代碼執(zhí)行順序的匣距,因為第二個函數(shù)的執(zhí)行依賴于第一個函數(shù)成功執(zhí)行并返回結(jié)果面哥。這段代碼可以用Continuation Pass Style(CPS)技術(shù)重寫,這樣一來add的返回值就不是傳給其調(diào)用者毅待,而是直接傳到square里去了尚卫。

    int j = add(5, 10, square);
    

    在上例中,add多了一個參數(shù):一個函數(shù)尸红,add必須在完成自己的計算后吱涉,調(diào)用這個函數(shù)并把結(jié)果傳給它。這時square就是add的一個continuation外里。上面兩段程序中j的值都是225怎爵。

    這樣,我們學(xué)習(xí)到了強制惰性語言順序執(zhí)行兩個表達式的第一個技巧盅蝗。再來看看下面IO程序(是不是有點眼熟鳖链?):

    System.out.println("Please enter your name: ");
    System.in.readLine();
    

    這兩行代碼彼此之間沒有依賴關(guān)系,因此編譯器可以隨意的重新安排它們的執(zhí)行順序风科∪雎郑可是只要用CPS重寫它乞旦,編譯器就必須順序執(zhí)行了,因為重寫后的代碼存在依賴關(guān)系了题山。

     System.out.println("Please enter your name: ", System.in.readLine);
    
  • 柯里化(局部調(diào)用(partial application))

    維基百科定義:

    是把接受多個參數(shù)函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù)兰粉,并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。

    當(dāng)一個函數(shù)沒有傳入全部所需參數(shù)時顶瞳,它會返回另一個函數(shù)(這個返回的函數(shù)會記錄那些已經(jīng)傳入的參數(shù))玖姑,這種情況叫作柯里化。

    在直覺上慨菱,柯里化聲稱“如果你固定某些參數(shù)焰络,你將得到接受余下參數(shù)的一個函數(shù)”。
    比如冪函數(shù)pow(x, y)符喝,它接受兩個參數(shù)——x和y闪彼,計算x^y。使用柯里化技術(shù)协饲,可以將y固定為2轉(zhuǎn)化為只接受單一參數(shù)x的平方函數(shù)畏腕,或者將y固定為3轉(zhuǎn)化為立方函數(shù)。代碼如下:

    // 平方函數(shù)
    double(^SquareFun)(double) = ^(double a){
        return pow(a, 2);
    };
    
    // 立方函數(shù)
    double(^CubeFun)(double) = ^(double a){
        return pow(a, 3);
    };
    

    熟悉設(shè)計模式的朋友已經(jīng)感覺到茉稠,currying完成的事情就是函數(shù)(接口)封裝描馅,它將一個已有的函數(shù)(接口)做封裝,得到一個新的函數(shù)(接口)而线,這與適配器模式(Adapter pattern)的思想是一致的铭污。

    ?

    為什么要柯里化:

    • 延遲計算。上面的例子已經(jīng)比較好地說明了膀篮。

    • 參數(shù)復(fù)用嘹狞。當(dāng)在多次調(diào)用同一個函數(shù),并且傳遞的參數(shù)絕大多數(shù)是相同的各拷,那么該函數(shù)可能是一個很好的柯里化候選刁绒。

    • 動態(tài)創(chuàng)建函數(shù)。這可以是在部分計算出結(jié)果后烤黍,在此基礎(chǔ)上動態(tài)生成新的函數(shù)處理后面的業(yè)務(wù),這樣省略了重復(fù)計算傻盟∷偃铮或者可以通過將要傳入調(diào)用函數(shù)的參數(shù)子集,部分應(yīng)用到函數(shù)中娘赴,從而動態(tài)創(chuàng)造出一個新函數(shù)规哲,這個新函數(shù)保存了重復(fù)傳入的參數(shù)(以后不必每次都傳)。

思考:高階函數(shù)中舉的例子sumWithFun如何柯里化诽表?
  • 只用"表達式"唉锌,不用"語句"

    "表達式"(expression)是一個單純的運算過程隅肥,總是有返回值;"語句"(statement)是執(zhí)行某種操作袄简,沒有返回值腥放。函數(shù)式編程要求,只使用表達式绿语,不使用語句秃症。也就是說,每一步都是單純的運算吕粹,而且都有返回值种柑。

    原因是函數(shù)式編程的開發(fā)動機,一開始就是為了處理運算(computation)匹耕,不考慮系統(tǒng)的讀寫(I/O)聚请。"語句"屬于對系統(tǒng)的讀寫操作,所以就被排斥在外稳其。

    當(dāng)然良漱,實際應(yīng)用中,不做I/O是不可能的欢际。因此母市,編程過程中,函數(shù)式編程只要求把I/O限制到最小损趋,不要有不必要的讀寫行為患久,保持計算過程的單純性。

    一切皆表達式思維:if b then 100 else 10浑槽,這不是條件跳轉(zhuǎn)蒋失,而是一個三元表達式,返回100或者10桐玻。

    ?

  • 不修改狀態(tài)

    上一點已經(jīng)提到篙挽,函數(shù)式編程只是返回新的值,不修改系統(tǒng)變量镊靴。因此铣卡,不修改變量,也是它的一個重要特點偏竟。
    在其他類型的語言中煮落,變量往往用來保存"狀態(tài)"(state)。不修改變量踊谋,意味著狀態(tài)不能保存在變量中蝉仇。函數(shù)式編程使用參數(shù)保存狀態(tài),最好的例子就是遞歸。下面的代碼是一個將字符串逆序排列的函數(shù)轿衔,它演示了不同的參數(shù)如何決定了運算所處的"狀態(tài)"沉迹。

    let a = 100,意義不是把100賦值給變量a害驹,而是把a符號綁定(或者叫匹配)到100鞭呕。

    由于變量值是不可變的,對于值的操作并不是修改原來的值裙秋,而是修改新產(chǎn)生的值琅拌,原來的值保持不便。例如一個Point類摘刑,其moveBy方法不是改變已有Point實例的x和y坐標(biāo)值进宝,而是返回一個新的Point實例。

    class Point(x: Int, y: Int){
        override def toString() = "Point (" + x + ", " + y + ")"
        def moveBy(deltaX: Int, deltaY: Int) = {
            new Point(x + deltaX, y + deltaY)
        }
    } 
    

    (示例來源:Anders Hejlsberg在echDays 2010上的演講)

    同樣由于變量不可變枷恕,純函數(shù)編程語言無法實現(xiàn)循環(huán)党晋,這是因為For循環(huán)使用可變的狀態(tài)作為計數(shù)器,而While循環(huán)或DoWhile循環(huán)需要可變的狀態(tài)作為跳出循環(huán)的條件徐块。因此在函數(shù)式語言里就只能使用遞歸來解決迭代問題未玻,這使得函數(shù)式編程嚴(yán)重依賴遞歸。

    ?

    通常來說胡控,算法都有遞推(iterative)和遞歸(recursive)兩種定義扳剿,以階乘為例,階乘的遞推定義為:
    而階乘的遞歸定義
    遞推定義的計算時需要使用一個累積器保存每個迭代的中間計算結(jié)果昼激,C代碼如下:

    static int fact(int n){
      int acc = 1;
      for(int k = 1; k <= n; k++){
        acc = acc * k;
      }
      return acc;
    }
    

    而遞歸定義的計算的C代碼如下:

    int fact(int n){
      if(n == 0) return 1
      return n * fact(n-1)
    }
    

    我們可以看到庇绽,沒有使用循環(huán),沒有使用可變的狀態(tài)橙困,函數(shù)更短小瞧掺,不需要顯示地使用累積器保存中間計算結(jié)果,而是使用參數(shù)n(在棧上分配)來保存中間計算結(jié)果凡傅。
    (示例來源:1. Recursion

    ?

  • pipeline

    這個技術(shù)的意思是辟狈,把函數(shù)實例成一個一個的action,然后夏跷,把一組action放到一個數(shù)組或是列表中哼转,然后把數(shù)據(jù)傳給這個action list,數(shù)據(jù)就像一個pipeline一樣順序地被各個函數(shù)所操作拓春,最終得到我們想要的結(jié)果释簿,如下的StringFunCompose函數(shù)。

    typedef NSString*(^StringBlock)(NSString*);
    
    StringBlock ToUpperCase =  ^(NSString*str){
        return [str uppercaseString];
    };
    
    StringBlock Excailm = ^(NSString*str){
        return [str stringByAppendingString:@"!"];
    };
    
    ////  可讀性不好硼莽,讓代碼從右向左運行,而不是由內(nèi)而外運行
    StringBlock Shout = ^(NSString* str){
        return Excailm(ToUpperCase(str));
    };
    
    StringBlock StringFunCompose(NSArray*blocks)
    {
        return ^(NSString* str){
            NSEnumerator *enumerator = [blocks reverseObjectEnumerator];
            StringBlock block;
            while (block = [enumerator nextObject]) {
                str = block(str);
            }
            return str;
        };
    }
    
    void sampleCurry(){
        NSLog(@"[sampleCurry]");
        NSLog(@"%@", Shout(@"Let's get started with FP"));
        NSLog(@"%@", StringFunCompose(@[Excailm, ToUpperCase])(@"Let's get started with FP"));
    }
    

    ?

    ?

函數(shù)式與面向?qū)ο蟆⒚嫦蜻^程區(qū)別

面向?qū)ο缶幊桃彩且环N命令式編程懂鸵。

命令式編程是面向計算機硬件的抽象偏螺,有變量(對應(yīng)著存儲單元),賦值語句(獲取匆光,存儲指令)套像,表達式(內(nèi)存引用和算術(shù)運算)和控制語句(跳轉(zhuǎn)指令),一句話终息,命令式程序就是一個馮諾依曼機的指令序列夺巩。面向?qū)ο笾皇墙7绞讲煌瑢嵸|(zhì)也是一種命令式編程周崭。

維基百科上說柳譬,函數(shù)式編程聲明式編程的一種。而聲明式編程則是與命令式編程相對的反義詞续镇。

  • 抽象思維方式

    函數(shù)式編程關(guān)心數(shù)據(jù)的映射美澳,命令式編程關(guān)心解決問題的步驟

    函數(shù)式考慮數(shù)據(jù)的變換過程,A->B->C->D->E摸航;而面向?qū)ο笾聘紤],我應(yīng)該有哪些對象酱虎,每個對象實現(xiàn)哪些過程函數(shù)雨膨,過程之間如何協(xié)作。

  • 變量無狀態(tài)读串,沒有賦值

    純函數(shù)式編程語言中的變量也不是命令式編程語言中的變量聊记,即存儲狀態(tài)的單元,而是代數(shù)中的變量爹土,即一個值的名稱甥雕。變量的值是不可變的(immutable),也就是說不允許像命令式編程語言中那樣多次給一個變量賦值胀茵。比如說在命令式編程語言我們寫“x = x + 1”社露,這依賴可變狀態(tài)的事實,拿給程序員看說是對的琼娘,但拿給數(shù)學(xué)家看峭弟,卻被認(rèn)為這個等式為假。

    ?

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

  • 沒有"副作用"

    所謂"副作用"(side effect)脱拼,指的是函數(shù)內(nèi)部與外部互動(最典型的情況瞒瘸,就是修改全局變量的值),產(chǎn)生運算以外的其他結(jié)果熄浓。
    函數(shù)式編程強調(diào)沒有"副作用"情臭,意味著函數(shù)要保持獨立,所有功能就是返回一個新的值,沒有其他行為俯在,尤其是不得修改外部變量的值竟秫。

    ?

  • 函數(shù)引用透明,是純函數(shù)

    引用透明(Referential transparency)跷乐,指的是函數(shù)的運行不依賴于外部變量或"狀態(tài)"肥败,只依賴于輸入的參數(shù),任何時候只要參數(shù)相同愕提,引用函數(shù)所得到的返回值總是相同的馒稍。

    有了不修改變量特性,這點是很顯然的浅侨。其他類型的語言纽谒,函數(shù)的返回值往往與系統(tǒng)狀態(tài)有關(guān),不同的狀態(tài)之下仗颈,返回值是不一樣的佛舱。這就叫"引用不透明",很不利于觀察和理解程序的行為挨决。

    ?

  • 可移植性/自文檔化(Portable / Self-Documenting)

    由于不依賴于函數(shù)外的變量请祖,因此可移植性好;純函數(shù)是完全自給自足的脖祈,它需要的所有東西都能輕易獲得肆捕。仔細(xì)思考思考這一點...這種自給自足的好處是什么呢?首先盖高,純函數(shù)的依賴很明確慎陵,因此更易于觀察和理解——沒有偷偷摸摸的小動作。

    ?

  • 代碼簡潔喻奥,開發(fā)快速

    函數(shù)式編程大量使用函數(shù)席纽,減少了代碼的重復(fù),因此程序比較短撞蚕,開發(fā)速度較快润梯。

    ?

  • 復(fù)用粒度最小

    函數(shù)式的利用粒度更小,以函數(shù)為單位甥厦。

    ?

  • 接近自然語言纺铭,易于理解

    函數(shù)式編程的自由度很高,可以寫出很接近自然語言的代碼刀疙。

    前文曾經(jīng)將表達式(1 + 2) * 3 - 4舶赔,寫成函數(shù)式語言:

      subtract(multiply(add(1,2), 3), 4)
    

    對它進行變形,不難得到另一種寫法:

      add(1,2).multiply(3).subtract(4)
    

    ? 這基本就是自然語言的表達了谦秧。再看下面的代碼竟纳,大家應(yīng)該一眼就能明白它的意思吧:

      merge([1,2],[3,4]).sort().search("2")
    

    ?

  • 易于測試撵溃,不容易出錯

    函數(shù)即不依賴外部的狀態(tài)也不修改外部的狀態(tài),函數(shù)調(diào)用的結(jié)果不依賴調(diào)用的時間和位置蚁袭,這樣寫的代碼容易進行推理征懈,不容易出錯石咬。這使得單元測試和調(diào)試都更容易揩悄。

    程序中的狀態(tài)不好維護,在并發(fā)的時候更不好維護鬼悠。(你可以試想一下如果你的程序有個復(fù)雜的狀態(tài)删性,當(dāng)以后別人改你代碼的時候,是很容易出bug的焕窝,在并行中這樣的問題就更多了)

    ?

  • 易于"并發(fā)編程"

    函數(shù)式編程不需要考慮"死鎖"(deadlock)蹬挺,因為它不修改變量,所以根本不存在"鎖"線程的問題它掂。不必?fù)?dān)心一個線程的數(shù)據(jù)巴帮,被另一個線程修改,所以可以很放心地把工作分?jǐn)偟蕉鄠€線程虐秋,部署"并發(fā)編程"(concurrency)榕茧。

    請看下面的代碼:

      var s1 = Op1();
      var s2 = Op2();
      var s3 = concat(s1, s2);
    

    由于s1和s2互不干擾,不會修改變量客给,誰先執(zhí)行是無所謂的用押,所以可以放心地增加線程,把它們分配在兩個線程上完成靶剑。其他類型的語言就做不到這一點蜻拨,因為s1可能會修改系統(tǒng)狀態(tài),而s2可能會用到這些狀態(tài)桩引,所以必須保證s2在s1之后運行缎讼,自然也就不能部署到其他線程上了。

    多核CPU是將來的潮流坑匠,所以函數(shù)式編程的這個特性非常重要血崭。

    ?

  • 代碼的熱升級

    函數(shù)式編程沒有副作用,只要保證接口不變笛辟,內(nèi)部實現(xiàn)是外部無關(guān)的届榄。所以,可以在運行狀態(tài)下直接升級代碼绿饵,不需要重啟娃闲,也不需要停機。Erlang語言早就證明了這一點围来,它是瑞典愛立信公司為了管理電話系統(tǒng)而開發(fā)的跺涤,電話系統(tǒng)的升級當(dāng)然是不能停機的匈睁。

    ?

函數(shù)式的缺點

  • 不擅長處理可變狀態(tài)和IO

    處理可變狀態(tài)和處理IO,要么引入可變變量桶错,要么通過Monad來進行封裝(如State Monad和IO Monad)航唆。

    System.out.println("Please enter your name: ");
    System.in.readLine();
    

    在惰性語言中沒人能保證第一行會中第二行之前執(zhí)行!這也就意味著我們不能處理IO院刁,不能調(diào)用系統(tǒng)函數(shù)做任何有用的事情(這些函數(shù)需要按照順序執(zhí)行糯钙,因為它們依賴于外部狀態(tài)),也就是說不能和外界交互了退腥!如果在代碼中引入支持順序執(zhí)行的代碼原語任岸,那么我們就失去了用數(shù)學(xué)方式分析處理代碼的優(yōu)勢(而這也意味著失去了函數(shù)式編程的所有優(yōu)勢)。

    ?

從函數(shù)式角度看面向?qū)ο?/h3>

? 看看維基百科中聲明式編程的定義:

  • 聲明式編程是告訴計算機需要計算“什么”而不是“如何”去計算

  • 任何沒有副作用的編程語言狡刘,或者更確切一點享潜,任何引用透明的編程語言

  • 任何有嚴(yán)格計算邏輯的編程語言

    ?

    我個人理解,這三個特點中的前兩個都是為了模仿人類的思維方式嗅蔬。

    ?

  • 因為人類思考主要靠直覺剑按,人類自己也搞不清楚自己怎么想出答案的。所以聲明式編程語言也不要規(guī)定電腦具體怎么執(zhí)行命令澜术。

  • 因為人類很固執(zhí)艺蝴,腦海中已有的概念很難修改。所以聲明式編程也要模仿人類瘪板,不允許修改變量吴趴。

  • 第三點則是人工智能先驅(qū)的美好愿望,像人類一樣思考侮攀,但是別像人類一樣犯錯锣枝。

  • 抽象方式

    • 面向?qū)ο笾g需要協(xié)作,導(dǎo)致對象之間關(guān)系錯綜復(fù)雜兰英,不好理清撇叁。

      建議:

      -[x] 可以將功能抽象成對象,但抽象的基礎(chǔ)是數(shù)據(jù)對象的映射畦贸,每個對象只實現(xiàn)自己的功能陨闹,和其它對象沒有任何耦合。
      -[x] 流程圖上的一個節(jié)點抽象成一個對象薄坏,最終有一個對象或者函數(shù)負(fù)責(zé)將所有的對象連接在一起趋厉,完成整個流程。這時候胶坠,可以從這個對象或者函數(shù)可以清晰地看到整個流程

  • 三大特性

    • 封裝

      • 可讀性差君账、調(diào)試?yán)щy

      面向?qū)ο笏枷雽⒆兞糠庋b到類里,然后使用函數(shù)對其進行操作沈善,成員變量的作用域在整個類乡数,需要考慮變量隨著時間的變化椭蹄。導(dǎo)致可讀性差,調(diào)試難度净赴。

      建議:

      ? 盡量少地使用成員變量绳矩,變量的作用范圍控制到最小

      • api使用困難

      由于函數(shù)和類成員變量偶合,在調(diào)用某個函數(shù)之前可能要先調(diào)用其它函數(shù)玖翅,如果順序不對翼馆,就可能導(dǎo)致異常。

      建議:

      -[x] 盡量使每個函數(shù)不引用成員變量和全局變量烧栋,使函數(shù)無副作用写妥,引用透明。
      -[x] 界面類只是簡單地響應(yīng)用戶事件及顯示數(shù)據(jù)审姓,盡量不要有邏輯。

?

  • 繼承

    • 需要理解繼承層次間類的關(guān)系祝峻,可讀性差魔吐,繼承層次大于3層時,就非常難理解莱找。

    • 使變量和函數(shù)的作用域擴大酬姆,使耦合度變大,可讀性進一步降低奥溺。

    • 使函數(shù)的作用域擴大辞色,導(dǎo)致一些函數(shù)覆蓋問題。

    建議:

    -[x] 不使用類

-[x] 少用繼承浮定,或者不繼承相满,使用了繼承,層次盡量不要超過3層
-[x] 多使用組合桦卒,少用繼承
  • 多態(tài)

    • 調(diào)試?yán)щy立美,需要運行時,才能確定具體哪個類實現(xiàn)了哪個函數(shù)方灾。

    建議:

    -[x] 使用高階函數(shù)和柯里化實現(xiàn)差異化

為什么函數(shù)式編程這么多優(yōu)點建蹄,沒有流行起來

  • 剛開始只是為了數(shù)學(xué)計算,有些場景無法實現(xiàn)裕偿,因此不適合實際工作應(yīng)用洞慎,特別是對界面的處理。不過也有用函數(shù)式實現(xiàn)界面的嘿棘,比如om
  • 由于大量使用遞歸劲腿,那時候的編譯器沒做優(yōu)化,導(dǎo)致運行緩慢蔫巩。

不僅最古老的函數(shù)式語言Lisp重獲青春谆棱,而且新的函數(shù)式語言層出不窮快压,比如Erlang、clojure垃瞧、Scala蔫劣、F#等等。目前最當(dāng)紅的Swift个从、C++脉幢、Objective-C、C#嗦锐、Python嫌松、Ruby、Javascript奕污,對函數(shù)式編程的支持都很強萎羔,就連老牌的面向?qū)ο蟮腏ava、面向過程的PHP碳默,都忙不迭地加入對匿名函數(shù)的支持贾陷。越來越多的跡象表明,函數(shù)式編程已經(jīng)不再是學(xué)術(shù)界的最愛嘱根,開始大踏步地在業(yè)界投入實用髓废。

也許繼"面向?qū)ο缶幊?之后,"函數(shù)式編程"會成為下一個編程的主流范式(paradigm)该抒。未來的程序員恐怕或多或少都必須懂一點慌洪。

java也在努力的改革,進步凑保,在像函數(shù)式“進化”冈爹,比如說java8提供的Stream流,lambda愉适,努力將函數(shù)(方法)提升為一等公民犯助,Stream中的透明化,無態(tài)化都流露著函數(shù)式的思想维咸。雖然java的fp現(xiàn)在可能走得是oop的極端表現(xiàn)形式剂买,但是也從另一個側(cè)面表達了fp的優(yōu)點和將來大勢所在。

作者:Accelerator鏈接:https://www.zhihu.com/question/30190384/answer/142902047來源:知乎著作權(quán)歸作者所有癌蓖。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)瞬哼,非商業(yè)轉(zhuǎn)載請注明出處。

現(xiàn)在流行起來租副,個人認(rèn)為有以下幾個原因:

  • 計算機硬件坐慰,多核技術(shù)發(fā)展使性能不再是瓶頸
  • fp編譯器的進步
  • 移動互聯(lián)網(wǎng)時代,分布式用僧、并發(fā)應(yīng)用程序的時代終于來臨了
  • 開源庫ReactiveCocoa结胀,RxJava及前端Redux使用了大量函數(shù)式的編程思想赞咙。
  • 大數(shù)據(jù),人工智能的發(fā)展導(dǎo)致只對數(shù)據(jù)進行處理糟港。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末攀操,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子秸抚,更是在濱河造成了極大的恐慌速和,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剥汤,死亡現(xiàn)場離奇詭異颠放,居然都是意外死亡,警方通過查閱死者的電腦和手機吭敢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門碰凶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人省有,你說我怎么就攤上這事痒留。” “怎么了蠢沿?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匾效。 經(jīng)常有香客問我舷蟀,道長,這世上最難降的妖魔是什么面哼? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任野宜,我火速辦了婚禮,結(jié)果婚禮上魔策,老公的妹妹穿的比我還像新娘匈子。我一直安慰自己,他們只是感情好闯袒,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布虎敦。 她就那樣靜靜地躺著,像睡著了一般政敢。 火紅的嫁衣襯著肌膚如雪其徙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天喷户,我揣著相機與錄音唾那,去河邊找鬼。 笑死褪尝,一個胖子當(dāng)著我的面吹牛闹获,可吹牛的內(nèi)容都是我干的期犬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼避诽,長吁一口氣:“原來是場噩夢啊……” “哼龟虎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茎用,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤遣总,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后轨功,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旭斥,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年古涧,在試婚紗的時候發(fā)現(xiàn)自己被綠了垂券。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡羡滑,死狀恐怖菇爪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柒昏,我是刑警寧澤凳宙,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站职祷,受9級特大地震影響氏涩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜有梆,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一是尖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泥耀,春花似錦饺汹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至陨囊,卻和暖如春弦疮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜘醋。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工胁塞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓啸罢,卻偏偏與公主長得像编检,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子扰才,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容