簡單整理一下對函數(shù)式編程的理解和實踐温自,本文包含函數(shù)式編程的概念、特性皇钞、合成悼泌、柯里化、尾調(diào)用夹界、尾遞歸優(yōu)化部分內(nèi)容馆里。
函數(shù)式編程最早起源于數(shù)學中的范疇論,和面向?qū)ο缶幊痰糁选⒚钍骄幊滩⒘袨槿缶幊谭妒揭舶荩簿褪侨绾尉帉懗绦虻姆椒ㄕ摗?它屬于"結(jié)構(gòu)化編程"的一種,主要思想是把運算過程盡量寫成一系列嵌套的函數(shù)調(diào)用趾痘。
范疇論的核心概念:
把世界上一切存在關系的元素理解為一個范疇慢哈,有點像分子結(jié)構(gòu),范疇內(nèi)部的各原子之間的關系就是函數(shù)永票,所以大概來說卵贱,范疇內(nèi)部包含:原子 + 函數(shù)(原子的關系映射)。
這么說就好理解了侣集,我們可以理解為我們真實的編程生產(chǎn)中所使用的一個個量稱之為原子键俱,對原子進行計算的函數(shù)即為函數(shù)。
像這樣:
// 命令式編程
const c = 1 + 2
console.log(c)
// 函數(shù)式編程
// 把add理解為是一個"范疇", a世分、b為范疇中的原子编振,內(nèi)部的"a + b"即為函數(shù)本身,即a和b的映射關系
// 整個計算過程未產(chǎn)生變量臭埋,對外部無依賴踪央、無改變、未泄露任何變量
const add = (a, b) => a + b
console.log(add(1, 2))
函數(shù)式編程的主要特征:
*一等公民
*計算過程完全函數(shù)化
*無副作用瓢阴,有輸入畅蹂,有輸出
*每個函數(shù)只有一個參數(shù)(柯里化)
*支持閉包和高階函數(shù)
*使用遞歸實現(xiàn)流程控制
合成
如果一個值要經(jīng)過多個函數(shù),才能變成另外一個值荣恐,就可以把所有中間步驟合并成一個函數(shù)液斜,這叫做"函數(shù)的合成"(compose)累贤。
示例:
// 兩個函數(shù)
const add = a => a + a
const multiply = c => c * c
multiply(add(1))
// 4
// 合成后
var compose = (a, b) => {
return c => {
return a(b(c))
}
}
// 簡寫
var compose = (a, b) => c => a(b(c))
// continue
const add = n => n + n
const multiply = n => n * n
compose(multiply, add)(2)
// 16
著作權(quán)歸作者所有。
商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)少漆,非商業(yè)轉(zhuǎn)載請注明出處臼膏。
作者:Surmon
鏈接:https://surmon.me/article/35
來源:Surmon.me
柯里化
所謂"柯里化",就是把一個多參數(shù)的函數(shù)检疫,轉(zhuǎn)化為單參數(shù)函數(shù)讶请。
示例:
const add = (a, b) => a + b
add(1, 2)
// 柯里化之后
const add = a => b => a + b
add(1)(2)
有了柯里化以后祷嘶,我們就能做到屎媳,所有函數(shù)只接受一個參數(shù),其實也是一種變相的封裝论巍。
如下實例:
// 如上文中烛谊,如果a量為固定值1,則我們可以封裝為
const addOne = b => add(1, b)
addOne(2)
// 等介于
add(1)(2)
尾調(diào)用
簡單一句話說就是:
函數(shù)內(nèi)部的最后一步操作是執(zhí)行另一個函數(shù)
如下代碼:
function a(x) {
return b(x)
}
函數(shù)a的最后一步操作是執(zhí)行函數(shù)b嘉汰,此函數(shù)即為尾調(diào)用丹禀。
以下兩種情況不屬于尾調(diào)用:
// 情況一
function f(x) {
let y = g(x)
return y
}
// 情況二
function f(x) {
return g(x) + 1
}
上面兩個函數(shù)f在內(nèi)部執(zhí)行完g函數(shù)后依然有其他操作,即便第二個函數(shù)的操作是寫在同一行內(nèi)的鞋怀,也不算尾調(diào)用双泪。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x)
}
上面代碼中函數(shù)m和n均為尾調(diào)用,他們都屬于函數(shù)f的最后一步操作密似。
尾調(diào)用優(yōu)化
尾調(diào)用之所以和其他調(diào)用不同焙矛,是因為,函數(shù)在執(zhí)行時残腌,在其內(nèi)部會維護一個調(diào)用堆棧村斟,保存調(diào)用位置和內(nèi)部變量等信息。
如果在函數(shù)A的內(nèi)部調(diào)用函數(shù)B抛猫,那么在A的調(diào)用記錄上方蟆盹,還會形成一個B的調(diào)用記錄。 等到B運行結(jié)束闺金,將結(jié)果返回到A逾滥,B的調(diào)用記錄才會消失。如果函數(shù)B內(nèi)部還調(diào)用函數(shù)C败匹,那就還有一個C的調(diào)用記錄棧寨昙,以此類推。 所有的調(diào)用記錄哎壳,就形成一個"調(diào)用棧"(call stack)毅待。
尾調(diào)用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調(diào)用記錄归榕,因為調(diào)用位置尸红、內(nèi)部變量等信息都不會再用到了,只要直接用內(nèi)層函數(shù)的調(diào)用記錄,取代外層函數(shù)的調(diào)用記錄就可以了外里。
// 下函數(shù)在內(nèi)存中執(zhí)行時
function f() {
let m = 1
let n = 2
return g(m + n)
}
f()
// 等同于
function f() {
return g(3)
}
f()
// 等同于
g(3)
上面代碼中怎爵,如果函數(shù)g不是尾調(diào)用,函數(shù)f就需要保存內(nèi)部變量m和n的值盅蝗、g的調(diào)用位置等信息鳖链,一直等到f函數(shù)完全執(zhí)行完畢才釋放。 但由于調(diào)用g之后墩莫,函數(shù)f就結(jié)束了芙委,所以執(zhí)行到最后一步,完全可以刪除 f() 的調(diào)用記錄狂秦,只保留 g(3) 的調(diào)用記錄灌侣。 這就叫做"尾調(diào)用優(yōu)化"(Tail call optimization),即只保留內(nèi)層函數(shù)的調(diào)用記錄裂问。 如果所有函數(shù)都是尾調(diào)用侧啼,那么完全可以做到每次執(zhí)行時,調(diào)用記錄只有一項堪簿,這將大大節(jié)省內(nèi)存痊乾。這就是"尾調(diào)用優(yōu)化"的意義。
尾遞歸
函數(shù)調(diào)用自身椭更,稱為遞歸哪审。如果尾調(diào)用自身,就稱為尾遞歸甜孤。
一個簡單的遞歸函數(shù):
// 當然這只是一個示例函數(shù)协饲,實際上是死循環(huán),瀏覽器會提示棧溢出
const a = n => {
return a(n++)
}
a(1)
遞歸非常耗費內(nèi)存缴川,因為需要同時保存成千上百個調(diào)用記錄茉稠,很容易發(fā)生"棧溢出"錯誤(stack overflow)。 但對于尾遞歸來說把夸,由于只存在一個調(diào)用記錄而线,所以永遠不會發(fā)生"棧溢出"錯誤。
function factorial(n) {
if (n === 1) return 1
return n * factorial(n - 1)
}
factorial(5) // 120
上面面代碼是一個階乘函數(shù)恋日,計算n的階乘膀篮,最多需要保存n個調(diào)用記錄,復雜度 O(n) 岂膳。 如果改寫成尾遞歸誓竿,只保留一個調(diào)用記錄,復雜度 O(1) 谈截。
function factorial(n, total) {
if (n === 1) return total
return factorial(n - 1, n * total)
}
factorial(5, 1) // 120
由此可見筷屡,"尾調(diào)用優(yōu)化"對遞歸操作意義重大涧偷,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。 ES6也是如此毙死,第一次明確規(guī)定燎潮,所有 ECMAScript 的實現(xiàn),都必須部署"尾調(diào)用優(yōu)化"扼倘。 這就是說确封,在 ES6 中,只要使用尾遞歸再菊,就不會發(fā)生棧溢出爪喘,相對節(jié)省內(nèi)存。
尾遞歸優(yōu)化
尾遞歸的實現(xiàn)袄简,往往需要改寫遞歸函數(shù)腥放,確保最后一步只調(diào)用自身泛啸。 做到這一點的方法绿语,就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù)。比如上面的例子候址,階乘函數(shù) factorial 需要用到一個中間變量 total 吕粹,那就把這個中間變量改寫成函數(shù)的參數(shù)。 這樣做的缺點就是不太直觀岗仑,第一眼很難看出來匹耕,為什么計算5的階乘,需要傳入兩個參數(shù)5和1稳其? 三個方法可以解決這個問題炸卑。
在尾遞歸函數(shù)之外盖文,再提供一個正常形式的函數(shù)。
function tailFactorial(n, total) {
if (n === 1) return total
return tailFactorial(n - 1, n * total)
}
function factorial(n) {
return tailFactorial(n, 1)
}
factorial(5) // 120
上面代碼通過一個正常形式的階乘函數(shù) factorial 洒敏,調(diào)用尾遞歸函數(shù) tailFactorial 凶伙,看起來就正常多了函荣。
柯里化
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n)
}
}
function tailFactorial(n, total) {
if (n === 1) return total
return tailFactorial(n - 1, n * total)
}
const factorial = currying(tailFactorial, 1)
factorial(5) // 120
上面代碼通過柯里化,將尾遞歸函數(shù) tailFactorial 變?yōu)橹唤邮?個參數(shù)的 factorial 煮落。
ES6的函數(shù)默認值
function factorial(n, total = 1) {
if (n === 1) return total
return factorial(n - 1, n * total)
}
factorial(5) // 120
上面代碼中蝉仇,參數(shù) total 有默認值1轿衔,所以調(diào)用時不用提供這個值睦疫。
總結(jié)一下: 遞歸本質(zhì)上是一種循環(huán)操作。純粹的函數(shù)式編程語言沒有循環(huán)操作命令蛤育,所有的循環(huán)都用遞歸實現(xiàn)瓦糕,這就是為什么尾遞歸對這些語言極其重要。 對于其他支持"尾調(diào)用優(yōu)化"的語言(比如Lua咕娄,ES6)圣勒,只需要知道循環(huán)可以用遞歸代替,而一旦使用遞歸圣贸,就最好使用尾遞歸旁趟。
注意:
ES6的尾調(diào)用優(yōu)化只在嚴格模式下開啟锡搜,正常模式是無效的耕餐。
這是因為在正常模式下肠缔,函數(shù)內(nèi)部有兩個變量哼转,可以跟蹤函數(shù)的調(diào)用棧槽华。
arguments返回調(diào)用時函數(shù)的參數(shù)
func.caller返回調(diào)用當前函數(shù)的那個函數(shù)
尾調(diào)用優(yōu)化發(fā)生時猫态,函數(shù)的調(diào)用棧會改寫,因此上面兩個變量就會失真勇凭。嚴格模式禁用這兩個變量虾标,所以尾調(diào)用模式僅在嚴格模式下生效灌砖。
本文轉(zhuǎn)自:https://surmon.me/article/35
著作權(quán)歸作者所有。
商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)周崭,非商業(yè)轉(zhuǎn)載請注明出處。
作者:Surmon