現在大公司的編程方式有:
1.oop(面向對象編程);
2.aop(面向切面編程);
3.函數式編程(JavaScript Functional Programming);
范疇論Category Theory
- 函數式編程是范疇論的數學分支是一門很復雜的數學,認為世界上所有概念體系都可以抽象出一個個范疇
- 彼此之間存在某種關系概念、事物、對象等等,都構成范疇。任何事物只要找出他們之間的關系,就能定義
- 箭頭表示范疇成員之間的關系,正式的名稱叫做“態射”(morphism)。范疇論認為,同一個范疇的所有成員,就是不同狀態的“變形”(transformation)。通過“態射”,一個成員可以變形成另一個成員
函數式編程5大特點
- 函數是
第一等公民
- 只用
表達式
,不用語句
- 沒有
副作用
- 不修改狀態
- 引用透明(函數運行只靠參數)
專業術語
- 純函數
- 函數的柯里化
- 函數組合
- Point Free
- 聲明式命令式代碼
- 惰性求值
純函數
對于相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態。
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
- 把一些對象自帶的方法轉化成純函數,不要命名轉瞬即逝的中間變量
- 這個函數中,我們使用了
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
需要學習
范疇與容器
- 我們可以把“范疇”想象成是一個容器,里面包含兩樣東西。值(value)、值的變形關系,也就是函數。
- 范疇論使用函數,表達范疇之間的關系。
- 伴隨著范疇論的發展,就發展出一整套函數的運算方法。這套方法起初只用于數學運算,后來有人將它在計算機上實現了,就變成了今天的“函數式編程”。
- 本質上,函數式編程只是范疇論的運算方法,跟數理邏輯、微積分、行列式式同一類東西,都是數學方法,只是碰巧他能用來寫程序。為什么函數式編程要求函數必須是純的,不能有副作用?因為它是一種數學運算,原始目的就是求值,不做其他事情,否則就無法滿足函數運算法則了。
函子是函數式編程里面最重要的數據類型,也是基本的運算單位和功能單位。它首先是一種范疇,也就是說,是一個容器,包含了值和變形關系。比較特殊的是,它的變形關系可以依次作用于每一個值,將當前容器變形成另一個容器。
容器、Functor(函子)
- $(...)返回的對象并不是一個原生的DOM對象,而是對于原生對象的一種封裝,這在某種意義上就是一個"容器"(但它并不函數式)
- Functor(函子)遵守一些特定規則的容器類型
- 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)