­

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函數式編程已經接近尾聲。我們到底學到了什麼?

首先,我們認識到了函數式編程的關注點是數據的映射關係,如何將一個數據結構更加優雅的轉化為另一個數據結構。函數式編程的主體是純函數,函數的內部實現不能影響到外部環境。

然後,我們學習了幾個常用的函數式編程場景——柯里化、偏函數、組合和管道。 幫助我們更好的實際業務中運用函數式編程。

最後,我們運用函子實現了靈活的同步鏈式調用函數。

參考鏈接:在你身邊你左右 –函數式編程別煩惱