函數式編程

現在大公司的編程方式有:

1.oop(面向對象編程);

2.aop(面向切面編程);

3.函數式編程(JavaScript Functional Programming);

范疇論Category Theory

  1. 函數式編程是范疇論的數學分支是一門很復雜的數學,認為世界上所有概念體系都可以抽象出一個個范疇
  2. 彼此之間存在某種關系概念、事物、對象等等,都構成范疇。任何事物只要找出他們之間的關系,就能定義
  3. 箭頭表示范疇成員之間的關系,正式的名稱叫做“態射”(morphism)。范疇論認為,同一個范疇的所有成員,就是不同狀態的“變形”(transformation)。通過“態射”,一個成員可以變形成另一個成員

函數式編程5大特點

  1. 函數是第一等公民
  2. 只用表達式,不用語句
  3. 沒有副作用
  4. 不修改狀態
  5. 引用透明(函數運行只靠參數)

專業術語

  1. 純函數
  2. 函數的柯里化
  3. 函數組合
  4. Point Free
  5. 聲明式命令式代碼
  6. 惰性求值

純函數

對于相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態。

var xs=[1,2,3,4,5];
//Array.slice是純函數,因為它沒有副作用,對于固定的輸入,輸出總是固定的
xs.slice(0,3); //[1,2,3]
xs.slice(0,3);  //[1,2,3]
xs.splice(0,3);   //[1,2,3]
xs.splice(0,3);  //[4,5]
import _ from 'lodash';
var sin=_.memorize(x=>Math.sin(x));
var a=sin(1); //第一次計算的時候會稍慢一點
var b=sin(1); //第二次有了緩存,速度極快
//純函數不僅可以有效降低系統的復雜度,還有很多很棒的特性,比如可緩存性
//惰性函數

不純

var min=18;
var checkage=function(age){
    return age>min; //依賴于外部的min,導致不純
}

函數的柯里化

傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

用柯里化來改造上面的不純函數

var checkage=min=>(age=>age>min);
var checkage18=checkage(18);
checkage18(20);

Point Free

  1. 把一些對象自帶的方法轉化成純函數,不要命名轉瞬即逝的中間變量
  2. 這個函數中,我們使用了str作為我們的中間變量,但這個中間變量除了讓代碼變得長了一點以外是毫無意義的
    const f=str=>str.toUpperCase().split('')

應用

var toUpperCase=word=>word.toUpperCase();
var split=x=>(str=>str.split(x));
var f=compose(split('').toUppercase);
f("abcd efgh");

這種風格能夠幫助我們減少不必要的命名,讓代碼保持簡潔和通用

聲明式與命令式代碼

命令式代碼的意思就是,我們通過編寫一條又一條指令去讓計算機執行一些動作,這其中一般都會涉及到很多繁雜的細節。而聲明式就要優雅很多了,我們通過寫表達式的方式來聲明我們想干什么,而不是通過一步一步的指示。

//命令式
let CEOs=[];
for(var i=0;i<companies.length;i++){
    CEOs.push(companies[i].CEO);
}
//聲明式
let CEOs=companies.map(c=>c.CEO);

優缺點

函數式編程的一個明顯的好處就是這種聲明式的代碼,對于無副作用的純函數,我們完全可以不考慮函數內部是如何實現的,專注于編寫業務代碼。優化代碼時,目光只需要集中在這些穩定堅固的函數內部即可。

相反,不純的函數式的代碼會產生副作用或者依賴外部系統環境,使用他們的時候總是要考慮這些不干凈的副作用。在復雜的系統中,這對于程序員的心智來說是極大的負擔。

惰性求值

function fn(){
    if(IE){//IE時
        fn=a;
    }else{//chrome時
        fn=b;
    }
    return fn;
}

第一次執行時會走if,然后fn重新賦值,第二次執行fn時,直接賦值不用判斷,提高執行效率

高階函數

函數當參數,把傳入的函數做一個封裝,然后返回這個封裝函數,達到更高程度的抽象

//命令式
var add=function(a,b){
    return a+b;
};
funtion math(func,array){
    return func(array[0],array[1]);
}
math(add,[1,2]); //3

尾調用優化

指函數內部的最后一個動作是函數調用。該調用的返回值,直接返回給函數。函數調用自身,稱為遞歸。如果尾調用自身,就稱為尾遞歸。遞歸需要保存大量的調用記錄,很容易發生棧溢出錯誤,如果使用尾遞歸優化,將遞歸變為循環,那么只需要保存一個調用記錄,這樣就不會發生棧溢出錯誤了。

//不是尾遞歸,無法優化
function factorial(n){
    if(n===1) return 1;
    return n*factorial(n-1);
}
//尾遞歸
function factorial(n,total){
    if(n===1) return total;
    return factorial(n-1,n*total);
}//ES6強制使用尾遞歸

普通遞歸時,內存需要記錄調用的堆棧所出的深度和位置信息。在最低層計算返回值,再根據記錄的信息,跳會上一層級計算,然后再跳回到更高一層,依次運行,直到最外層的調用函數。在cpu計算和內存會消耗很多,而且當深度過大時,會出現堆棧溢出

function sum(x){
    if(x===1) return 1;
    return x+sum(x-1);
}
sum(5);  //15  遞歸
function sum(x,total){
    if(x===1) return x+total;
    return sum(x-1,x+total);
}
sum(5,0); 
sum(4,5);
sum(3,9);
sum(2,12);
sum(1,14);
15 //尾遞歸,每次執行之后,函數重新傳入參數,直到結束

整個計算過程是線性的,調用一次sum(x,total)后,會進入下一個棧,相關的數據信息和跟隨進入,不再放在堆棧上保存。當計算完最后的值之后,直接返回到最上層的sum(5,0).這能有效的防止堆棧溢出。
在ECMAScript6,我們將迎來尾遞歸優化,通過尾遞歸優化,javascript代碼在解釋成機器碼的時候,將會向while看起,也就是說,同時擁有數學表達能力和while的效能。

閉包

自己領會

函數式編程比較火熱的庫

  • Rxjs //截流與仿抖
  • cyclejs
  • lodashjs
  • underscorejs //開始學最佳的庫
  • ramadajs

需要學習

范疇與容器

  1. 我們可以把“范疇”想象成是一個容器,里面包含兩樣東西。值(value)、值的變形關系,也就是函數。
  2. 范疇論使用函數,表達范疇之間的關系。
  3. 伴隨著范疇論的發展,就發展出一整套函數的運算方法。這套方法起初只用于數學運算,后來有人將它在計算機上實現了,就變成了今天的“函數式編程”。
  4. 本質上,函數式編程只是范疇論的運算方法,跟數理邏輯、微積分、行列式式同一類東西,都是數學方法,只是碰巧他能用來寫程序。為什么函數式編程要求函數必須是純的,不能有副作用?因為它是一種數學運算,原始目的就是求值,不做其他事情,否則就無法滿足函數運算法則了。

函子是函數式編程里面最重要的數據類型,也是基本的運算單位和功能單位。它首先是一種范疇,也就是說,是一個容器,包含了值和變形關系。比較特殊的是,它的變形關系可以依次作用于每一個值,將當前容器變形成另一個容器。

容器、Functor(函子)

  1. $(...)返回的對象并不是一個原生的DOM對象,而是對于原生對象的一種封裝,這在某種意義上就是一個"容器"(但它并不函數式)
  2. Functor(函子)遵守一些特定規則的容器類型
  3. Functor是一個對于函數調用的抽象,我們賦予容器自己去調用函數的能力。把東西裝進一個容器,只留出一個接口map給容器外的函數,map一個函數時,我們讓容器自己來運行這個函數,這樣容器就可以自由地選擇何時何地如何操作這個函數,以致于擁有惰性求值、錯誤處理、一步調用等非常牛掰的特性
var Container=function(x){
    this.__value=x;
}
//函數式編程一般約定,函子有一個of方法
Container.of=x=>new Container(x);
//Container.of('abcd);
//一般約定,函子的標志就是容器具有map方法。該方法將容器里面的每一個值,映射到另一個容器。
Container.prototype.map=function(f){
    return Container.of(f(this.__value));
}
Container.of(3)
    .map(x=>x+1)                //Container(4)
    .map(x=>'Result is '+x);        //Container('Result is 4')

Maybe 函子

函子接受各種函數,處理容器內部的值,這里就有一個問題,容器內部的值可能是一個空值(比如null),而外部函數未必有處理空值的機制,如果傳入空值,很可能就會出錯。

Functor.of(null).map(function(s){
    return s.toUpperCase();
});
//TypeError
class Maybe extends Functor{
    map(f){
        return this.val?Maybe.of(f(this.val)):Maybe.of(null);
    }
}
Maybe.of(null).map(function(s){
    return s.toUpperCase();
});
//Maybe(null) //報錯,未定義
var Maybe=function(x){
    this.__value=x;
}
Maybe.of=function(x){
    return new Maybe(x);
}
Maybe.prototype.map=function(f){
    return this.isNothing()?Maybe.of(null):Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing=function(){
    return (this.__value===null||this.__value===undefined);
}
Maybe(null) //不會報錯了
//新的容器我們稱為Maybe

Either 函子

條件運算if...else 是常見的運算之一,函數式編程里面,使用Either函子表達。Either函子內部有兩個值:左值(left)和右值(right)。右值是正常情況下使用的值,左值是右值不存在時使用的默認值。

class Either extends Functor{
    constructor(left,right){
        this.left=left;
        this.right=right;
    }
    map(f){
        //右值存在變右值,否則變左值
        return this.right?Either.of(this.left,f(this.right)):Either.of(f(this.left),this.right);
    }
}
Either.of=function(left,right){
    return new Either(left,right);
}

var addOne=function(x){
    return x+1;
}
Either.of(5,6).map(addOne); //Either(5,7);
Either.of(1,null).map(addOne);  //Either(2);
Either
    //右值中有address這個屬性,則覆蓋原來的xxx,否則使用默認的xxx
    .of({address:'xxx'},currentUser.address)    .map(updateField);

es5寫法

錯誤處理、Either

var Left=function(x){
    this.__value=x;
}
var Rigth=function(x){
    this.__value=x;
}
Left.of=function(x){
    return new Left(x);
}
Right.of=function(x){
    return new Right(x);
}
Left.prototype.map=functin(f){
    return this;
}
Right.prototype.map=function(f){
    return Right.of(f(this.__value));
}

Left和Right唯一的區別就在于map方法的實現,Right.map的行為和我們之前提到的map函數一樣。但是Left.map就很不同了:它不會對容器做任何事情,只是很簡單地把這個容器拿進來又扔出去。這個特性意味著,Left可以用來傳遞一個錯誤消息。

var getAge = user => user.age ? Right.of(user.age):Left.of("Error");;
getAge({name:'stark',age:'21'}).map(age=>'Age is '+age);
//Right('Age is 21');
getAge({name:'stark'}).map({age=>'Age is '+age});
//Left('Error');

Left 可以讓調用鏈中任意一環的錯誤立即返回到調用鏈的尾部,這給我們錯誤處理帶來了很大的方便,再也不用一層又一層的Try/catch

AP因子

函子里面包含的值,完全可能是函數。我們可以想象這樣一種情況,一個函子的值是數值,另一個函子的值是函子。

class Ap extends Functor{
    ap(F){
        return Ap.of(this.val(F.val));
    }
}
Ap.of(addTwo).ap(Functor.of(2));

實例

function Functor(val){
    this.__val=val;
}
Functor.of=function(val){
    return new Functor(val);
}
Functor.prototype.map=function(fn){
    return Functor.of(fn(this.__val));
}
function addTwo(x){
    return x+2;
}
function Ap(val){
    Functor.call(this,val);
}
Ap.of=function(val){
    return new Ap(val);
}
var __proto=Object.create(Functor.prototype);
__proto.constructor=Ap.prototype.constructor;
Ap.prototype=__proto;
Ap.prototype.ap=function(F){
    return Ap.of(this.__val(F.__val));
}

const A=Functor.of(2);
const B=Ap.of(addTwo);
console.log(B.ap(A));  //4
//console.log(B.ap(A).ap(A));此時會報錯,B.ap(A)其中A不是個函數

IO

真正的程序總要去接觸骯臟的世界

function readLoaclStorage(){
    return window.localStorage;
}

Io跟前面那幾個Functor不同的地方在于,他的__value是一個函數。它把不純的操作(比如IO、網絡請求、DOM)包裹到一個函數內,從而延遲這個操作的執行。所以我們認為,IO包含的是被包裹的操作的返回值。

IO其實也算是惰性求值

IO負責了調用鏈積累了很多很多不純的操作,帶來的復雜性和不可維護性。

import _ from 'lodash';
var compose=_.flowRight;
var IO=function(f){
    this._value=f;
}
IO.of=x=>new IO(_=>x);
IO.prototype.map=function(f){
    return new IO(compose(f,this.__value));
}

Monad

Monad就是一種設計模式,表示將一個運算過程,通過函數拆解成互相連接的多個步驟。你只要提供下一步運算所需的函數,整個運算就會自動進行下去。

Promise就是一種Monad

Monad糖我們避開了嵌套地獄,可以輕松地進行深度嵌套的函數式編程,比如IO和其它異步任務

Maybe.of(
    Maybe.of(
        Maybe.of({name:'Mulburry',number:99})
    )
)
class Monad extends Functor{
    join(){
        return this.val;
    }
    flatMap(f){
        return this.map(f).join();
    }
}

Monad 函子的作用是,總是返回一個單層的函子。它有一個flatMap方法,與map方法作用相同,唯一的區別是如果生成了一個嵌套函子,他會取出后這內部的值,保證返回的永遠是一個單層的容器,不會出現嵌套的情況。

如果函數f返回的是一個函子,那么this.map(f)就會生成一個嵌套的函子。所以,join方法保證了flatMap方法總是返回一個單層的函子。這意味著嵌套的漢子會被鋪平(flatMap)

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

推薦閱讀更多精彩內容