函數式編程(二)

純函數

函數式編程中的函數,指的就是純函數,這也是整個函數式編程的核心
純函數:相同的輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用。
純函數就類似數學中的函數(用來描述輸入和輸出之間的關系),y = f(x)

綠色的就是對函數的輸入,藍色的就是對函數輸出,f就是函數,就是輸入輸出的關系

  • lodash 是一個純函數的功能庫,提供了對數組、數字、對象、字符串、函數等操作的一些方法

來感受下啥叫純與不純
數組的 slice 和 splice 分別是:純函數和不純的函數
slice: 返回數組中的指定部分,不會改變原數組
splice: 對數組進行操作返回該數組,會改變原數組

let numbers = [1, 2, 3, 4, 5]
// 純函數
numbers.slice(0, 3)// => [1, 2, 3]
numbers.slice(0, 3)// => [1, 2, 3]
numbers.slice(0, 3)// => [1, 2, 3]
// 不純的函數
numbers.splice(0, 3)// => [1, 2, 3]
numbers.splice(0, 3)// => [4, 5]
numbers.splice(0, 3)// => []

可以看到每次都是相同的輸入,slice每次都是相同的輸出,所以他是純函數,由于splice會改變原數組,雖然相同輸入,每次輸出都變了,所以不純

  • 函數式編程不會保留計算中間的結果,所以變量是不可變的(無狀態的)
  • 我們可以把一個函數的執行結果交給另一個函數去處理
    就比如這個slice這個純函數,我們在調用他的時候會傳遞參數,然后會獲取結果,而函數內部的結果我們是無法獲取到的,也就是他不會保留內部的計算中間的結果,所以我們認為函數式編程的變量是不可變的
    所以在基于函數式編程的過程中,我么會經常需要一些細粒度的純函數,要是自己去寫細粒度的純函數,要寫非常多,并不方便,所以我們有好多函數式編程的庫,比如說Lodash,有了這些細粒度的函數,我們可以把一個函數的執行結果交給另一個函數去處理,我們就能組合出功能更強大函數

Lodash

去官網看看吧,都是些好用的方法,https://www.html.cn/doc/lodash/

純函數的好處

  • 可緩存

因為純函數對相同的輸入始終有相同的結果,所以可以把純函數的結果緩存起來
假設有一個超復雜的計算的函數,比如要計算地球的體積,他的入參是地球半徑,每計算一次要耗時一秒,那么我們就可以把計算結果儲存起來,因為對相同的輸入始終有相同的結果,其中lodash里有這么個記憶函數memoize

const _ = require('lodash')
function getVolume (r) {
    console.log("半徑是" + r)
    return 4 / 3 * Math.PI * r * r *r
}
let getVolumeWithMemory = _.memoize(getVolume)
console.log(getVolumeWithMemory(10))
console.log(getVolumeWithMemory(10))
console.log(getVolumeWithMemory(10))
// 輸出:
// 半徑是10
// 4188.79
// 4188.79
// 4188.79

被memoize包裹后形成的getVolumeWithMemory,入參和getVolume是一樣的,可以看到半徑是10只執行了一遍,10=>4188.79這個結果就被存起來了,下次再遇到,就直接拿出結果了,不需要再執行了。

我們自己模擬一個memoize

function memoize (f) {
  let cache = {} 
// 這個cache就是用來緩存結果的,用f的入參當key,用出參當value,比如上面的getVolumeWithMemory執行完后就形成了{10 : 4188.79}
  return function () {
    let key = JSON.stringify(arguments) // arguments可能是個數組或其他形式,所以轉成字符串
    cache[key] = cache[key] || f.apply(f, arguments) // cache[key]存在就取cache[key],不存在就執行f,因為arguments是數組所以用apply方法
    return cache[key]
  }
}
  • 可測試
    純函數讓測試更方便

  • 并行處理
    在多線程環境下并行操作共享的內存數據很可能會出現意外情況
    純函數不需要訪問共享的內存數據,所以在并行環境下可以任意運行純函數 (es6 新增的Web Worker,讓js有了多線程能力)

副作用

純函數:對于相同的輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用

// 不純的
let mini = 18
function checkAge (age) {
return age >= mini
}
//你看那個不純的,你敢保證每次輸入20都返回true么,因為它依賴了一個外部變量,無法知曉此變量何時會被篡改

// 純的(有硬編碼,后續可以通過柯里化解決)
function checkAge (age) {
let mini = 18
return age >= mini
}

副作用讓一個函數變的不純(如上例),純函數的根據相同的輸入返回相同的輸出,如果函數依賴于外部的狀態就無法保證輸出相同,就會帶來副作用。

副作用的來源除了一些全局變量,還有配置文件,數據庫,獲取用戶的輸入等等
所有的外部交互都有可能帶來副作用,副作用也使得方法通用性下降不適合擴展和可重用性,同時副作用會給程序中帶來安全隱患給程序帶來不確定性,比如用戶的賬號密碼是要存在數據庫而非函數里的,所以副作用不可能完全禁止,但要盡可能控制它們在可控范圍內發生。

柯里化

這里我們先上一個案例,用柯里化來解決上一個例子中硬編碼的問題

function checkAge (age) {
    let mini = 18
    return age >= mini
}
// 既然有硬編碼,那我們通過把min字段傳進去,不就解決了么,于是
// 普通純函數
function checkAge (age, min) {
   return age >= min
}
checkAge(20, 18)
checkAge(24, 18)
checkAge(26, 30)

// 假設經常以18,30為基準值,每次都輸入18,30就過于重復了
// 想想我們之前在閉包那里是怎么處理的

// 柯里化
function checkAge (min) { //既然經常是基準值不變的,所以就讓基準值通過閉包儲存起來
    return function (age) {
        return age >= min
    }
}
let checkAge18 = checkAge(18)
let checkAge30 = checkAge(30)
checkAge18(24) // 再判斷數字的時候就不用輸入18了,這就是函數柯里化
checkAge18(20)

// ES6 寫法
let checkAge = min => (age => age >= min)

柯里化:

  • 當一個函數有多個參數的時候先傳遞一部分參數調用它(這部分參數以后永遠不變)
  • 然后返回一個新的函數接收剩余的參數,返回結果

我們看看普通純函數變成柯里化函數的過程是不是就是如上定義一樣,還挺套路的

Lodash中的柯里化

既然如此套路,Lodash中也有通用的柯里化方法curry
_.curry(func)

  • 功能:創建一個函數,該函數接收一個或多個 func 的參數,如果 func 所需要的參數都被提供則執行 func 并返回執行的結果。否則繼續返回該函數并等待接收剩余的參數。
  • 參數:需要柯里化的函數
  • 返回值:柯里化后的函數
const _ = require('lodash')
// 要柯里化的函數
function getSum (a, b, c) {
    return a + b + c
}
// 柯里化后的函數
let curried = _.curry(getSum)
// 測試
curried(1, 2, 3) 
curried(1)(2)(3)
curried(1, 2)(3) // 輸出結果都是6

柯里化讓多元函數轉變成了一元函數(幾個入參就是幾元函數,上面的getSum就是三元函數)

柯里化案例

用上述的curry方法來實現一個案例:提取一個字符串中的所有數字

// 面向過程的方式, 正則match
''.match(/\d+/g) // 數字

// 如果改成提取數組中含有數字的元素,上面的方法就不通用了,所以我們用柯里化來包裝一下
const _ = require('lodash')
let match = _.curry(function (reg, str) {
    return str.match(reg)
})
// 讓他具有特定功能
let findStrNum = match(/\d+/g) //尋找數字
let findStrSpace = match(/\s+/g) //尋找空格
// 試一試
console.log(findStrNum('asd1234')) // true
console.log(findStrSpace('asd1234')) // false
// OK,到這里,關于字符串的match就都可以實現了


// 現在我們要繼續用上面方法,來實現數組中的提取含有數字的項
// 數組需要循環,我們來一個柯里化的filter
let filter = function (fn, arr) { // 這里的fn是要做操作的函數
   return arr.filter(fn) // 這個filter是數組的filter方法,別搞混
}
let filterCurry = _.curry(filter) // 柯里化
// 讓他具有特定功能, 那么就可以傳進去上面定義的findStrNum
let findArrNum = filterCurry(findStrNum)
// 試一試
console.log(findArrNum(['abc123', 'abc']))

最后我們就用函數式的方式,實現了這個功能,可能覺得這樣寫非常麻煩,還不如自己去正則寫來實現,但是你要清楚的是,將來這些函數,我們可以不停的重復使用。

const _ = require('lodash')
let match = _.curry(function (reg, str) {
    return str.match(reg)
})
let findStrNum = match(/\d+/g)
let findStrSpace = match(/\s+/g)
let filterCurry = _.curry(function (fn, arr) {
   return arr.filter(fn)
})
let findArrNum = filterCurry(findStrNum)
let findArrSpace = filterCurry(findStrSpace)

看看上面這些東西,定義一次,就可以在你的工程中無數次的重復使用,能避免自己造輪子中的小bug

模擬 _.curry() 的實現

// 先來看一下這玩意當時是如何使用的
const _ = require('lodash')
function getSum (a, b, c) {
    return a + b + c
}
let curried = _.curry(getSum)
curried(1, 2, 3) // 它的調用形式分為傳入全部參數,或部分參數
curried(1, 2)(3) 

// 自己實現
function curry (func) {
    return function curriedFn (...args) {
    // 判斷實參和形參的個數
    if (args.length < func.length) {
        return function () {
            return curriedFn(...args.concat(Array.from(arguments))) // 這里面就是把每次的參數結合起來,再次調用curriedFn
        }
    }
    // 實參和形參個數相同,調用 func,返回結果
    return func(...args)
    }
}

柯里化總結

  • 柯里化可以讓我們給一個函數傳遞較少的參數得到一個已經記住了某些固定參數的新函數
  • 這是一種對函數參數的'緩存'
  • 讓函數變的更靈活,讓函數的粒度更小
  • 可以把多元函數轉換成一元函數,可以組合使用函數產生強大的功能

下篇再整理下函數的組合,以避免柯里化后洋蔥圈似的代碼
函數式編程(三)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373