函數(shù)式編程(二)

純函數(shù)

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

綠色的就是對函數(shù)的輸入,藍(lán)色的就是對函數(shù)輸出,f就是函數(shù),就是輸入輸出的關(guān)系

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

來感受下啥叫純與不純
數(shù)組的 slice 和 splice 分別是:純函數(shù)和不純的函數(shù)
slice: 返回?cái)?shù)組中的指定部分,不會改變原數(shù)組
splice: 對數(shù)組進(jìn)行操作返回該數(shù)組,會改變原數(shù)組

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

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

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

Lodash

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

純函數(shù)的好處

  • 可緩存

因?yàn)榧兒瘮?shù)對相同的輸入始終有相同的結(jié)果,所以可以把純函數(shù)的結(jié)果緩存起來
假設(shè)有一個(gè)超復(fù)雜的計(jì)算的函數(shù),比如要計(jì)算地球的體積,他的入?yún)⑹堑厍虬霃?,每?jì)算一次要耗時(shí)一秒,那么我們就可以把計(jì)算結(jié)果儲存起來,因?yàn)閷ο嗤妮斎胧冀K有相同的結(jié)果,其中l(wèi)odash里有這么個(gè)記憶函數(shù)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,入?yún)⒑蚲etVolume是一樣的,可以看到半徑是10只執(zhí)行了一遍,10=>4188.79這個(gè)結(jié)果就被存起來了,下次再遇到,就直接拿出結(jié)果了,不需要再執(zhí)行了。

我們自己模擬一個(gè)memoize

function memoize (f) {
  let cache = {} 
// 這個(gè)cache就是用來緩存結(jié)果的,用f的入?yún)?dāng)key,用出參當(dāng)value,比如上面的getVolumeWithMemory執(zhí)行完后就形成了{(lán)10 : 4188.79}
  return function () {
    let key = JSON.stringify(arguments) // arguments可能是個(gè)數(shù)組或其他形式,所以轉(zhuǎn)成字符串
    cache[key] = cache[key] || f.apply(f, arguments) // cache[key]存在就取cache[key],不存在就執(zhí)行f,因?yàn)閍rguments是數(shù)組所以用apply方法
    return cache[key]
  }
}
  • 可測試
    純函數(shù)讓測試更方便

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

副作用

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

// 不純的
let mini = 18
function checkAge (age) {
return age >= mini
}
//你看那個(gè)不純的,你敢保證每次輸入20都返回true么,因?yàn)樗蕾嚵艘粋€(gè)外部變量,無法知曉此變量何時(shí)會被篡改

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

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

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

柯里化

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

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

// 假設(shè)經(jīng)常以18,30為基準(zhǔn)值,每次都輸入18,30就過于重復(fù)了
// 想想我們之前在閉包那里是怎么處理的

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

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

柯里化:

  • 當(dāng)一個(gè)函數(shù)有多個(gè)參數(shù)的時(shí)候先傳遞一部分參數(shù)調(diào)用它(這部分參數(shù)以后永遠(yuǎn)不變)
  • 然后返回一個(gè)新的函數(shù)接收剩余的參數(shù),返回結(jié)果

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

Lodash中的柯里化

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

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

柯里化讓多元函數(shù)轉(zhuǎn)變成了一元函數(shù)(幾個(gè)入?yún)⒕褪菐自瘮?shù),上面的getSum就是三元函數(shù))

柯里化案例

用上述的curry方法來實(shí)現(xiàn)一個(gè)案例:提取一個(gè)字符串中的所有數(shù)字

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

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


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

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

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)

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

模擬 _.curry() 的實(shí)現(xiàn)

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

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

柯里化總結(jié)

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容