學會使用函數式編程的程序員(第2部分)

圖片描述

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!前端

本系列的第一篇:git

  1. 學會使用函數式編程的程序員(第1部分)

組合函數 (Function Composition)

圖片描述

做爲程序員,咱們是懶惰的。咱們不想構建、測試和部署咱們編寫的一遍又一遍的代碼。咱們老是試圖找出一次性完成工做的方法,以及如何重用它來作其餘事情。程序員

代碼重用聽起來很棒,可是實現起來很難。若是代碼業務性過於具體,就很難重用它。如時代碼太過通用簡單,又不多人使用。因此咱們須要平衡二者,一種製做更小的、可重用的部件的方法,咱們能夠將其做爲構建塊來構建更復雜的功能。github

在函數式編程中,函數是咱們的構建塊。每一個函數都有各自的功能,而後咱們把須要的功能(函數)組合起來完成咱們的需求,這種方式有點像樂高的積木,在編程中咱們稱爲 組合函數編程

看下如下兩個函數:segmentfault

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};

上面寫法有點冗長了,咱們用箭頭函數改寫一下:數組

var add10 = value => value + 10;
var mult5 = value => value * 5;

如今咱們須要有個函數將傳入的參數先加上 10 ,而後在乘以 5, 以下:閉包

var mult5AfterAdd10 = value => 5 * (value + 10)

儘管這是一個很是簡單的例子,但仍然不想從頭編寫這個函數。首先,這裏可能會犯一個錯誤,好比忘記括號。第二,咱們已經有了一個加 10 的函數 add10 和一個乘以 5 的函數 mult5 ,因此這裏咱們就在寫已經重複的代碼了。函數式編程

使用函數 add10mult5 來重構 mult5AfterAdd10函數

var mult5AfterAdd10 = value => mult5(add10(value));

咱們只是使用現有的函數來建立 mult5AfterAdd10,可是還有更好的方法。

在數學中, f ∘ g 是函數組合,叫做「f 由 g 組合」,或者更常見的是 「f after g」。 所以 (f ∘ g)(x) 等效於f(g(x)) 表示調用 g 以後調用 f

在咱們的例子中,咱們有 mult5 ∘ add10 或 「add10 after mult5」,所以咱們的函數的名稱叫作 mult5AfterAdd10。因爲Javascript自己不作函數組合,看看 Elm 是怎麼寫的:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

Elm 中 << 表示使用組合函數,在上例中 value 傳給函數 add10 而後將其結果傳遞給 mult5。還能夠這樣組合任意多個函數:

f x =
   (g << h << s << r << t) x

這裏 x 傳遞給函數 t,函數 t 的結果傳遞給 r,函數 t 的結果傳遞給 s,以此類推。在Javascript中作相似的事情,它看起來會像 g(h(s(r(t(x))))),一個括號噩夢。

Point-Free Notation

圖片描述

Point-Free Notation就是在編寫函數時不須要指定參數的編程風格。一開始,這風格看起來有點奇怪,可是隨着不斷深刻,你會逐漸喜歡這種簡潔的方式。

multi5AfterAdd10 中,你會注意到 value 被指定了兩次。一次在參數列表,另外一次是在它被使用時。

// 這個函數須要一個參數

mult5AfterAdd10 value =
    (mult5 << add10) value

可是這個參數不是必須的,由於該函數組合的最右邊一個函數也就是 add10 指望相同的參數。下面的 point-free 版本是等效的:

// 這也是一個須要1個參數的函數

mult5AfterAdd10 =
    (mult5 << add10)

使用 point-free 版本有不少好處。

  1. 首先,咱們不須要指定冗餘的參數。因爲沒必要指定參數,因此也就沒必要考慮爲它們命名。
  2. 因爲更簡短使得更容易閱讀。本例比較簡單,想象一下若是一個函數有多個參數的狀況。

天堂裏的煩惱

圖片描述

到目前爲止,咱們已經瞭解了組合函數如何工做以及如何經過 point-free 風格使函數簡潔、清晰、靈活。

如今,咱們嘗試將這些知識應用到一個稍微不一樣的場景。想象一下我使用 add 來替換 add10

add x y =
    x + y
mult5 value =
    value * 5

如今如何使用這兩個函數來組合函數 mult5After10 呢?

咱們可能會這樣寫:

-- 這是錯誤的!!!

mult5AfterAdd10 =
    (mult5 << add) 10

但這行不通。爲何? 由於 add 須要兩個參數。

這在 Elm 中並不明顯,請嘗試用Javascript編寫:

var mult5AfterAdd10 = mult5(add(10)); // 這個行不通

這段代碼是錯誤的,可是爲何?

由於這裏 add 函數只能獲取到兩個參數(它的函數定義中指定了兩個參數)中的一個(實際只傳遞了一個參數),因此它會將一個錯誤的結果傳遞給 mult5。這最終會產生一個錯誤的結果。

事實上,在 Elm 中,編譯器甚至不容許你編寫這種格式錯誤的代碼(這是 Elm 的優勢之一)。

咱們再試一次:

var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free

這個不是point-free風格可是我以爲還行。可是如今我再也不僅僅組合函數。我在寫一個新函數。一樣若是這個函數更復雜,例如,我想使用一些其餘的東西來組合mult5AfterAdd10,我真的會遇到麻煩。

因爲咱們不能將這個兩個函數對接將會出現函數組合的做用受限。這太糟糕了,由於函數組合是如此強大。

若是咱們能提早給add函數一個參數而後在調用 mult5AfterAdd10 時獲得第二個參數那就更好了。這種轉化咱們叫作 柯里化

柯里化 (Currying)

圖片描述

Currying 又稱部分求值。一個 Currying 的函數首先會接受一些參數,接受了這些參數以後,該函數並不會當即求值,而是繼續返回另一個函數,剛纔傳入的參數在函數造成的閉包中被保存起來。待到函數被真正須要求值的時候,以前傳入的全部參數都會被一次性用於求值

上例咱們在組合函數 mult5add(in) 時遇到問題的是,mult5 使用一個參數,add 使用兩個參數。咱們能夠經過限制全部函數只取一個參數來輕鬆地解決這個問題。我只需編寫一個使用兩個參數但每次只接受一個參數的add函數,函數柯里化就是幫咱們這種工做的。

柯里化函數一次只接受一個參數。

咱們先賦值 add 的第1個參數,而後再組合上 mult5,獲得 mult5AfterAdd10 函數。當 mult5AfterAdd10 函數被調用的時候,add 獲得了它的第 2 個參數。

JavaScript 實現方式以下:

var add = x => y => x + y

此時的 add 函數前後分兩次獲得第 1 個和第 2 個參數。具體地說,add函數接受單參x,返回一個也接受單參 y的函數,這個函數最終返回 x+y 的結果。

如今能夠利用這個 add 函數來實現一個可行的 mult5AfterAdd10* :

var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));

compose 有兩個參數 fg,而後返回一個函數,該函數有一個參數 x,並傳給函數 f,當函數被調用時,先調用函數 g,返回的結果做爲函數 f的參數。

總結一下,咱們到底作了什麼?咱們就是將簡單常見的add函數轉化成了柯里化函數,這樣add函數就變得更加自由靈活了。咱們先將第1個參數10輸入,而當mult5AfterAdd10函數被調用的時候,最後1個參數纔有了肯定的值。

柯里化與重構(Curring and Refactoring)

圖片描述

函數柯里化容許和鼓勵你分隔複雜功能變成更小更容易分析的部分。這些小的邏輯單元顯然是更容易理解和測試的,而後你的應用就會變成乾淨而整潔的組合,由一些小單元組成的組合。

例如,咱們有如下兩個函數,它們分別將輸入字符串用單花括號和雙花括號包裹起來:

bracketed = function (str) {
  retrun "{" + str + "}"
}
    
doubleBracketed = function (str) {
  retrun "{{" + str + "}}"
}

調用方式以下:

var bracketedJoe =  bracketed('小智')

var doubleBracketedJoe =  doubleBracketed('小智')

能夠將 bracketdoubleBracket 轉化爲更變通的函數:

generalBracket = function( prefix , str ,suffix ) {
  retrun  prefix ++ str ++ suffix
}

但每次咱們調用 generalBracket 函數的時候,都得這麼傳參:

var bracketedJoe = generalBracket("{", "小智", "}")

var doubleBracketedJoe = generalBracket("{{", "小智", "}}")

以前參數只須要輸入1個,但定義了2個獨立的函數;如今函數統一了,每次卻須要傳入3個參數,這個不是咱們想要的,咱們真正想要的是一箭雙鵰。

由於生成小括號雙括號功能但一,從新調整一下 咱們將 generalBracket 三個參數中的 prefix,str 各柯里化成一個函數,以下:

generalBracket = function( prefix ) {
  return  function( suffix ){
      return function(str){
          return prefix + str + suffix
      }
  }
}

這樣,若是咱們要打印單括號或者雙括號,以下:

// 生成單括號
var bracketedJoe = generalBracket('{')('}')
bracketedJoe('小智') // {小智}

// 生成雙括號
var bracketedJoe = generalBracket('{{')('}}')
bracketedJoe('小智') // {{小智}}

常見的函數式函數(Functional Function)

函數式語言中3個常見的函數:Map,Filter,Reduce

圖片描述

以下JavaScript代碼:

for (var i = 0; i < something.length; ++i) {
      // do stuff
 }

這段代碼存在一個很大的問題,但不是bug。問題在於它有不少重複代碼(boilerplate code)。若是你用命令式語言來編程,好比Java,C#,JavaScript,PHP,Python等等,你會發現這樣的代碼你寫地最多。這就是問題所在

如今讓咱們一步一步的解決問題,最後封裝成一個看不見 for 語法函數:

先用名爲 things 的數組來修改上述代碼:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // 警告:值被改變!
}
console.log(things); // [10, 20, 30, 40]

這樣作法很不對,數值被改變了!

在從新修改一次:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

這裏沒有修改things數值,但卻卻修改了newThings。暫時先無論這個,畢竟咱們如今用的是 JavaScript。一旦使用函數式語言,任何東西都是不可變的。

如今將代碼封裝成一個函數,咱們將其命名爲 map,由於這個函數的功能就是將一個數組的每一個值映射(map)到新數組的一個新值。

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

函數 f 做爲參數傳入,那麼函數 map 能夠對 array 數組的每項進行任意的操做。

如今使用 map 重寫以前的代碼:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

這裏沒有 for 循環!並且代碼更具可讀性,也更易分析。

如今讓咱們寫另外一個常見的函數來過濾數組中的元素:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};

當某些項須要被保留的時候,斷言函數 pred 返回TRUE,不然返回FALSE。

使用過濾器過濾奇數:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

比起用 for 循環的手動編程,filter 函數簡單多了。最後一個常見函數叫reduce。一般這個函數用來將一個數列歸約(reduce)成一個數值,但事實上它能作不少事情。

在函數式語言中,這個函數稱爲 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() 有2個參數
    return acc;
});

reduce函數接受一個歸約函數 f,一個初始值 start,以及一個數組 array

這三個函數,map,filter,reduce能讓咱們繞過for循環這種重複的方式,對數組作一些常見的操做。但在函數式語言中只有遞歸沒有循環,這三個函數就更有用了。附帶提一句,在函數式語言中,遞歸函數不只很是有用,還必不可少。

原文:

https://medium.com/@cscalfani...
https://medium.com/@cscalfani...

編輯中可能存在的bug無法實時知道,過後爲了解決這些bug,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具Fundebug

你的點贊是我持續分享好東西的動力,歡迎點贊!

歡迎加入前端你們庭,裏面會常常分享一些技術資源。

clipboard.png

相關文章
相關標籤/搜索