前面二篇學習了函數式編程的基本概念和常見用法。今天,我們來學習函數式編程的最后一個概念——函子(Functor)。
相信有一部分同學對這個概念很陌生,畢竟現在已經有很多成熟的輪子,基本能滿足我們日常的業務開發,所以沒必須重復造輪子。但是,作為一名(未來)優秀的程序員,光會用怎么能行呢?必須要理解更深層的思想。下面就來學習函子部分的知識...
函子(Functor)
在正式學習函子之前,我會先拋出一個問題,先用普通的方式解決,然后轉換為用函子解決,這能幫助我們更好的理解函子。同時,這也是我想說的,在我們學習一個新的知識點前,首先必須清楚為什么會有它,或者說它是為了解決什么問題而生的,這也是我們學習新知識后能夠快速達到學以致用的最有效方法,不然很容易被遺忘。
function double (x) {
return x * 2
}
function add5 (x) {
return x + 5
}
var a = add5(5)
double(a)
// 或者
double(add5(5))
我們現在想以數據為中心,串行的方法去執行,即:
(5).add5().double()
很明顯,這樣的串行調用清晰多了。下面我們就實現一個這樣的串行調用:
要實現這樣的串行調用,需要(5)
必須是一個引用類型,因為需要掛載方法。同時,引用類型上要有可以調用的方法也必須返回一個引用類型,保證后面的串行調用。
class Num {
constructor (value) {
this.value = value ;
}
add5 () {
return new Num( this.value + 5)
}
double () {
return new Num( this.value * 2)
}
}
var num = new Num(5);
num.add5 ().double ()
我們通過new Num(5) ,創建了一個 num 類型的實例。把處理的值作為參數傳了進去,從而改變了 this.value 的值。我們把這個對象返會出去,可以繼續調用方法去處理數據。
通過上面的做法,我們已經實現了串行調用。但是,這樣的調用很不靈活。如果我想再實現個減一的函數,還要再寫到這個 Num 構造函數里。所以,我們需要思考如何把對數據處理這一層抽象出來,暴露到外面,讓我們可以靈活傳入任意函數。來看下面的做法:
class Num {
constructor (value) {
this.value = value ;
}
map (fn) {
return new Num( fn(this.value) )
}
}
var num = new Num(5);
num.map(add5).map(double)
我們創建了一個 map 方法,把處理數據的函數 fn 傳了進去。這樣我們就完美的實現了抽象,保證的靈活性。
到這里,我們的函子就該正式登場了。不用怕,其實函子的概念很簡單,我們在上面其實已經創建了一個函子雛形。現在我們整理一下,創建一個真正的函子:
class Functor{
constructor (value) {
this.value = value ;
}
map (fn) {
return Functor.of(fn(this.value))
}
}
Functor.of = function (val) {
return new Functor(val);
}
Functor.of(5).map(add5).map(double)
現在我們可以用Functor.of(5).map(add5).map(double)
去調用,是不是覺得清爽多了。
下面總結一下這個函子的幾個特征:
- Functor 是一個容器,它包含了值,就是this.value(想一想你最開始的new Num(5))
- Functor 具有 map 方法。該方法將容器里面的每一個值,映射到另一個容器。(想一想你在里面是不是new Num(fn(this.value))
- 函數式編程里面的運算,都是通過函子完成,即運算不直接針對值,而是針對這個值的容器----函子。(想一想你是不是沒直接去操作值)
- 函子本身具有對外接口(map方法),各種函數就是運算符,通過接口接入容器,引發容器里面的值的變形。(說的就是你傳進去那個函數把 this.value 給處理了)
- 函數式編程一般約定,函子有一個 of 方法,用來生成新的容器。(就是幫我們 new 了一個對象出來)
說了那么多,如果還是不理解函子概念的話,那也正常。因為仔細看看這也沒什么的嘛,就是封裝了一個簡單的構造函數而已,咋就整出來一個新概念函子了呢?不理解不重要,主要是看到了函子幫我們更好的串行調用函數處理數據。回想一下我們上一節學的 compose,是不是很像呢?只是函子的調用方式顯得更加優雅。
現在,我們已經認識了一個基礎的函子。接下來,我們需要認識一個更加完善的函子——Maybe函子...
Maybe 函子
我們知道,在做字符串處理的時候,如果一個字符串是 null, 那么對它進行 toUpperCase() 就會報錯。
Functor.of(null).map(value => value.toUpperCase())
所以我們需要對 null 值進行特殊過濾:
class Maybe{
constructor (value) {
this.value = value;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
}
Maybe.of = function (val) {
return new Maybe(val);
}
var a = Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
我們看到只需要把在中設置一個空值過濾,就可以完成這樣一個 Maybe 函子。是不是so easy。
Monad 函子
Monad 函子也是一個函子,其實很原理簡單,只不過在原有的基礎上又加了一些功能。那我們來看看它與其它的 有什么不同吧。
我們知道,函子是可以嵌套函子的。比如下面這個例子:
function fn (e) { return e.value }
var a = Maybe.of( Maybe.of( Maybe.of('str') ) )
console.log(a);
console.log(a.map(fn));
console.log(a.map(fn).map(fn));
我們有時候會遇到一種情況,需要處理的數據是 Maybe {value: Maybe}。顯然我們需要一層一層的解開。這樣很麻煩,那么我們有沒有什么辦法得到里面的值呢?
class Monad {
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
join ( ) {
return this.value;
}
}
Monad.of = function (val) {
return new Monad(val);
}
這樣,我們就能很輕易的處理嵌套函子的問題了:
var a = Monad.of( Monad.of('str') )
console.log(a.join().map(toUpperCase))
Modan函子也是一個很簡單的概念,僅僅多了個 join 函數,為我們處理嵌套函子。
總結
至此,js函數式編程已經接近尾聲。我們到底學到了什么?
首先,我們認識到了函數式編程的關注點是數據的映射關系,如何將一個數據結構更加優雅的轉化為另一個數據結構。函數式編程的主體是純函數,函數的內部實現不能影響到外部環境。
然后,我們學習了幾個常用的函數式編程場景——柯里化、偏函數、組合和管道。 幫助我們更好的實際業務中運用函數式編程。
最后,我們運用函子實現了靈活的同步鏈式調用函數。
參考鏈接:在你身邊你左右 --函數式編程別煩惱