JavaScript ES6函數式編程(三):函子
- 2019 年 10 月 27 日
- 筆記
前面二篇學習了函數式編程的基本概念和常見用法。今天,我們來學習函數式編程的最後一個概念——函子(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函數式編程已經接近尾聲。我們到底學到了什麼?
首先,我們認識到了函數式編程的關注點是數據的映射關係,如何將一個數據結構更加優雅的轉化為另一個數據結構。函數式編程的主體是純函數,函數的內部實現不能影響到外部環境。
然後,我們學習了幾個常用的函數式編程場景——柯里化、偏函數、組合和管道。 幫助我們更好的實際業務中運用函數式編程。
最後,我們運用函子實現了靈活的同步鏈式調用函數。
參考鏈接:在你身邊你左右 –函數式編程別煩惱