函數(shù)式編程的那些特性(一): Immutability

Functional Programming特性(一): Immutability

FBI warning:
這又是一個系列的文章(Hopefully),介紹Functional Programming的一些特性,以及Why there are so awesome! 目測將cover以下的一些話題:

  1. Immutability
  2. Pure function
  3. No side-effect
  4. High order function
  5. Curried Function
  6. Pattern matching
  7. Effect managements.(Monads, STM,...)
  8. Rethink OOP
  9. ......

目前沒有辦法確定更新頻率,甚至沒有辦法保證一定會更新下去,只能說我盡量努力一個星期更新一遍,but I can’t give you my word.
另外,我相應(yīng)你已經(jīng)發(fā)現(xiàn)了,這些文章(Like every other posts)將會充斥著各種英文單詞或句子。部分是為了裝逼,部分是因為當(dāng)我寫到那里的時候,就想這么表達而已,感覺很帶勁。所以,慎重關(guān)注,不喜還請輕噴!

這幾天一直在折騰Elm,這門函數(shù)式編程語言,應(yīng)該說,感覺真是酸爽。使用一門新的語言,在一個小的Community里面,還真的會有不少的問題,不少的坑。這些坑被你踩到的時候,可能還不能很快的得到解決,你要費一番功夫,你要Google得時間長一點,你要耐心的去看每一個相關(guān)的github issue、pull request,甚至相關(guān)的源碼。此外,IDE的支持也是個大問題,可能每一個編輯器/IDE,都有不足的地方.比如說對于Elm開發(fā),我現(xiàn)在的解決方案是,同時使用WebStorm(for editing files)和Atom(for linting),利用Mac的分屏功能,一邊一個,這想想都覺得搞笑??!但是,問到問題,解決問題,我想這也是程序員的一大樂趣吧!

Anyway,這個系列的文章將介紹一下函數(shù)式編程Functional Programming,以下簡稱FP)的一些特性。當(dāng)然首先要解釋的問題是,什么是函數(shù)式編程?這個概念我相信大家已經(jīng)聽爛了,但是究竟什么叫函數(shù)式編程呢?我們知道這個概念是跟面向?qū)ο缶幊桃粯?,是一種編程模式,但是它跟OO有什么不同呢?有些人可能知道這是比OO更早出現(xiàn)的一種編程模式,但是為什么它沒有像OO一想,如今得到如此廣泛的應(yīng)用?又是什么原因,最近這幾年的編程界,大家又都在討論這個東西?

我們一個一個問題來看,首先看看FP在Wikipedia的定義

In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions[1] or declarations[2] instead of statements. In functional code, the output value of a function depends only on the arguments that are input to the function, so calling a function f twice with the same value for an argument x will produce the same result f(x) each time. Eliminating side effects, i.e. changes in state that do not depend on the function inputs, can make it much easier to understand and predict the behavior of a program, which is one of the key motivations for the development of functional programming.

其實我認(rèn)為,上面的描述基本把FP的核心給概括到了,那就是:

  1. Immutable data,數(shù)據(jù)是不可變的(Immutability)
  2. 一個函數(shù)的輸出只依賴于這個函數(shù)的輸入(pure function)
  3. 任何一個函數(shù)都沒有side effect:一個函數(shù)除了操縱它的輸入,然后返回輸出之外,再也不做其它的事情。

這里提到三個特性,基本上每一種函數(shù)式編程語言都有比這三個多出很多的特性和概念,而且這些特性和概念在每一種函數(shù)式編程語言里面基本都是類似的,然而我認(rèn)為,這三個特性才是函數(shù)式編程最根本的特性,是跟其它編程模式(OO、過程式編程)最截然不同的地方。

單單把這三個特性列出來,聽起來有點虛,有點理論,有點模糊,那么接下來我用具體的例子來說明一個,這三個到底是什么樣的東東,對于我們習(xí)慣了OO的人來說,寫起代碼來有什么樣的一種感受。這里先介紹第一個:

Immutability

這個包括兩層含義:1. 沒有變量,只有常量,任何一個“變量”一旦被賦值,就不能再次被賦值。類似于所以的變量都是用final(in Java)或const(in C/C++/JS/etc)修飾過了的;2. 數(shù)據(jù)一旦被創(chuàng)建出來,就不能被修改了。比如一個數(shù)組被創(chuàng)建出來以后,就不能再增、刪、改里面的元素了;一個Map(Hash、Dictionary)被創(chuàng)建出來以后,里面的key-value pair也是不能再變的的,也不能add key,也不能remove key,一個對象/Struct一旦被創(chuàng)建,里面的每一個field都不能被改變了。這個特性聽起來有點奇怪,什么要這么設(shè)定呢?這個特性對我們編程有什么影響呢?

變量的噩夢

看看以下的代碼:

age = 18    
happy_new_year()
print(age)  //What is the output

上面這段小代碼,對于非FP來說,age是可以變的,所以你必須去看happy_new_year里面的代碼,才知道age最后到底是什么樣的值。而對于FP來說,你不用去管happy_new_year()里面的代碼是什么樣的,你就能確定age的值一定是18。因為變量age一旦被賦值,就不會被改變了。
所以,從這個簡單的例子就可以看出,F(xiàn)P可以極大的提高代碼的可讀性(可理解性)。
現(xiàn)在,想象一下,你最近接觸的OO代碼里面有多少可變的成員變量?為了控制某一個地方的邏輯而專門設(shè)置的狀態(tài)變量?多少可變的臨時變量?這些變量給你造成了多大的思想負(fù)擔(dān)?你要翻遍所以可能改變這個變量的地方,又有哪些public方法會改變這個變量?甚至外面又有哪些地方調(diào)用了這個方法,你可能都要找清楚,有的時候一個變量可能不會被改變,但是你還是要去找一遍,因為你不知道這個值不會被改變,所以你翻遍了所有的地方,結(jié)果發(fā)現(xiàn),這個值沒有被改變。。。回想一下你就會發(fā)現(xiàn),這簡直就是一個噩夢!
也許你會說,對于哪些初始化以后不會被改變的變量,你可以用final來修飾一下,但是你回想一下,你有多少次會真的這么去做呢?多數(shù)人用final,是因為定義常量或者是一個臨時變量要被某個匿名內(nèi)部類引用到,所以不得不用final。再說,就算你是個優(yōu)秀的程序員(如果一個程序員會因為考慮到一個變量初始化以后不會被改變而特地加一個final,那我認(rèn)為他確實是個優(yōu)秀的程序員!),就算你看過Effective Java,你會記得該加final的地方加final,那又有多大比例人能像你這么優(yōu)秀呢?再說,這還是解決不了那些確實會被改變的變量引起的謎之代碼,這依然是個噩夢!
而且,噩夢還不會這里結(jié)束,想象一下如果happy_new_year這個方法是異步的,里面起了一個新的線程去改變age的值呢?在這種情況下,age的值就更難判斷了,雖然你說,可以用一些加鎖的方式去做同步,但是你不能否認(rèn),寫好多線程的代碼并不是那么容易的。而且,這些額外的控制代碼,只會讓代碼的可讀性進一步的降低。所以毫不夸張的說,這簡單就是升級版的噩夢!

可變的數(shù)據(jù)(結(jié)構(gòu))

可變的數(shù)據(jù)結(jié)構(gòu)也是同樣的道理。我相信以下的代碼很多安卓的同學(xué)都寫過:

public class SomeActivity extends Activity {
    private List<...> mDataList = new ArrayList<>();    
    //....
    void someMethod() {
        ListView listView = (ListView)findViewById(R.id.some_list_view)
        mDataList = ...  //一個List類型的成員變量。
        YourAdapter adater = new YourAdapter(...., mDataList)
        listView.setAdapter(adapter);
    }
}

如果你不是寫安卓的孩子,這里簡單解釋一下,以上的代碼就是在界面上顯示一個列表。列表里面的每一項對應(yīng)于mDataList這個List里面的一項數(shù)據(jù),adater就是一個將mDataList里面的每一項數(shù)據(jù)轉(zhuǎn)變成ListView里面的一行的工具。但是你發(fā)現(xiàn)某些時候,你的ListView上面顯示的數(shù)據(jù)與你預(yù)期的可能不一樣,或許多了一些,或許少了一些,或許某一項是錯誤的,你費了好多時間調(diào)試下來,發(fā)現(xiàn)原來是在某些不經(jīng)意的地方,你修改了mDataList里面的值。但是這個mDataList怎么會影響到ListView呢?哦,原來你的adater里面也有一個data list,那個data list跟你Actvity里面的mDataList引用的是同一個對象,類似于這樣的實現(xiàn):

public class YourAdapter extends BaseAdapter {
    private List<...> dataList; //
    public BaseAdapter(..., List<...> data) {
        ...
        this.dataList = data;
    }
}

這種情況其實是非常的counter-intuitive的,一般來說當(dāng)你看到

    YourAdapter adapter = new YourAdapter(...., mDataList)
    listView.setAdapter(adapter);

的時候,你不會再想到adater里面的數(shù)據(jù)跟外面的mDataList還有關(guān)系,你會以為adapter取了mDataList當(dāng)時那一刻的數(shù)據(jù),后面除非你手動往adapter里面增刪數(shù)量,不然里面的數(shù)據(jù)是不會變的。所以如果你的Adapter也是這種情況,那你就得無時無刻不意識到,mDataListadapter里面的dataList是同一個對象,要非常小心的修改里面的數(shù)據(jù),這無疑給人增加了非常大的心里負(fù)擔(dān)。此外,這個mDataList會不會在某個地方被傳給另外一個對象?那個對象會不會在某個地方修改了這個List?它又會不會再次傳給別人???更糟糕的是,一般來說你的代碼不是你一個人在維護,其它人知道這個mDataListadapter之間的關(guān)系嗎?如果他不清楚,在某個地方無意間修改了mDataList的值怎么辦?如果他寫了一段代碼,傳給了別的對象怎么辦?同時,adapter本身會不會有一個方法return這個list給別人,那個人會不會修改這個List??????我相信你看到這里,你自己頭都大了。但是你別急,因為還有更糟糕的情況,那就是很多時候你需要異步的修改mDataList里面的值,這個時候又需要加鎖,需要同步,從而代碼可讀性進一步降低,這又回到上面那個升級版的噩夢了。
當(dāng)然,像上面的情況,解決的辦法也有很多,其中一個比較簡單的是在你的adapter里面不要持有跟外面?zhèn)鬟M去的list同一個引用,而是自己new一個,當(dāng)外面把數(shù)據(jù)傳進來的時候,把里面的數(shù)據(jù)自己copy一份。這樣,外面對mDataList的修改就不會影響到adapter了,類似于下面這樣:

public class YourAdapter extends BaseAdapter {
    private final List<...> dataList = new ArrayList<>(); //我用final我自豪!
    public BaseAdapter(..., List<...> data) {
        ...
        if (data != null) {
            dataList.addAll(data);
        }
    }
}

這個其實也是Effective Java里面推薦的Best Practices之一(Item39: Make defensive copies when needed)。Again,這需要一個程序員有比較高的覺悟,需要他有這方面的意識,知道這里有個坑需要注意,還需要他有比較高的水平(比如看過Effective Java并且會照著做)。覺悟、意識、水平,這些都是很難控制的事情,也不是很容易、不是一時半會就能培養(yǎng)出來的事情。
總之,Mutability is Evil!

那FP在這里的作用是怎么樣的呢?那就是他解決了這個問題。因為里面的數(shù)據(jù)、“變”量都是不可變的,所以上面所說的問題統(tǒng)統(tǒng)不存在!想想,這是不是極大的提高了代碼的可讀性,可理解性,極大的減少了人的心理負(fù)擔(dān)?
因為age是不可變的,所以無論happy_new_year()里面的代碼是怎么樣的,你都不用關(guān)心,你可以立刻的肯定,print(age)的輸出一定是18,無論有多少個線程,都是這樣的。
因為mDataList是不可變的,所以就算adapter持有的跟mDataList同一個引用也沒關(guān)系,你永遠(yuǎn)不用擔(dān)心mDataList會改變,從而也不用擔(dān)心對它的改變會不會影響到adapter,無論mDataList被傳給多少個其它對象都無所謂,無論有多少個線程都無所謂。
那如果我需要改變age的值怎么辦(比如27天以后你就老了一歲)?如果我需要往mDataList里面加數(shù)據(jù)怎么辦?答案很簡單,你創(chuàng)建另外一個“變”量來存放它們改變以后的值,然后在需要使用這些新值的地方用那個新的“變”量就好了。

new_age = age + 1
List<...> newDataList = mDataList.add(...);

我相信看到這里,有一個強烈的反應(yīng)馬上出現(xiàn)在你心里:這太夸張了,這豈不是需要額外增加非常多的對象?這豈不是會極大的增加GC的負(fù)擔(dān)?
對于這個問題,我的答案是,這個擔(dān)心完全是沒必要的,多數(shù)的FPL(L for Language)都會在底層實現(xiàn)解決這個問題,比如共用內(nèi)存。比如在上面的newDataListmDataList中,這兩個List共用mDataList現(xiàn)有的內(nèi)存空間,只不過newDataList多了一個元素的內(nèi)存而已。因為這片內(nèi)存是不可變的,所以這個共用完全是安全的。當(dāng)然不同的語言、不同的情況可能會有不同的方式來處理這個問題。總之,目前還沒有哪種FPL說因為immutability而導(dǎo)致GC負(fù)擔(dān)太重的問題。

“但是我總有一些地方確實需要“變量”啊,我要用來表示對象當(dāng)前的狀態(tài)、系統(tǒng)當(dāng)前的狀態(tài),當(dāng)前用戶的狀態(tài),這些東西沒有變量怎么行呢?”
你有這個疑問,是因為你的思想還停留在OO的編程模式中,在FP中是不會這樣去思考問題的,F(xiàn)P is about transforming data, not about maintaining state. There is much less "state" stuff in FP.
不明白?那就跟著這個系列,后面你將看到,沒有“state”的編程是怎么樣一種體驗。

小結(jié)

這篇文章介紹了FP的immutability特性,為什么mutability這么糟糕,為什么immutability這么有必要,不知道講得是不是夠清楚,有任何問題、疑問、反對意見,歡迎留言!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,334評論 2 377

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