函數(shù)式編程

簡單整理一下對函數(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

另附上個人小站xiaoxiekeke和github主頁https://github.com/xiaoxiekeke续镇,歡迎star和follow我哦!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摸航,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子酱虎,更是在濱河造成了極大的恐慌,老刑警劉巖擂涛,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件读串,死亡現(xiàn)場離奇詭異,居然都是意外死亡撒妈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門杰捂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來棋蚌,“玉大人蒿往,你說我怎么就攤上這事盛垦∪柯” “怎么了赌蔑?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵俯在,是天一觀的道長。 經(jīng)常有香客問我娃惯,道長跷乐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任趾浅,我火速辦了婚禮愕提,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘皿哨。我一直安慰自己浅侨,他們只是感情好,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布证膨。 她就那樣靜靜地躺著如输,像睡著了一般。 火紅的嫁衣襯著肌膚如雪央勒。 梳的紋絲不亂的頭發(fā)上不见,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音崔步,去河邊找鬼稳吮。 笑死,一個胖子當著我的面吹牛井濒,可吹牛的內(nèi)容都是我干的灶似。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼瑞你,長吁一口氣:“原來是場噩夢啊……” “哼酪惭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捏悬,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤撞蚕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后过牙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甥厦,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡纺铭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了刀疙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舶赔。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谦秧,靈堂內(nèi)的尸體忽然破棺而出竟纳,到底是詐尸還是另有隱情,我是刑警寧澤疚鲤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布锥累,位于F島的核電站,受9級特大地震影響集歇,放射性物質(zhì)發(fā)生泄漏桶略。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一诲宇、第九天 我趴在偏房一處隱蔽的房頂上張望际歼。 院中可真熱鬧,春花似錦姑蓝、人聲如沸鹅心。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旭愧。三九已至,卻和暖如春虐秋,著一層夾襖步出監(jiān)牢的瞬間榕茧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工客给, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肢簿。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓靶剑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親池充。 傳聞我的和親對象是個殘疾皇子桩引,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容