一杯茶的時間,上手函數(shù)式編程
最近和一些同學討論了函數(shù)式編程,很多同學總覺得聽起來很高大上,但用起來卻無從下手。于是我抽時間捋了捋,將平時工作中用到的函數(shù)式編程案例和思想整理了出來,相信閱讀本文后,大家都能快速上手函數(shù)式編程。
函數(shù)式編程目前使用范圍非常廣,常用的框架,語言幾乎都能看到它的身影。
前端框架:react、vue 的 hooks 用法。 打包工具:webpack 的 webpack-chain 用法。 工具庫:underscore、lodash、ramda。 部署方式:serverless。 后端:java、c# 中的 lamda 表達式。
本文將通過以下 3 個部分來深入函數(shù)式編程。
編程范式 函數(shù)式編程 函數(shù)式編程常見案例
編程范式
編程范式?指的是一種編程風格,它描述了程序員對程序執(zhí)行的看法。在編程的世界中,同一個問題,可以站在多個角度去分析解決,這些不同的解決方案就對應(yīng)了不同的編程風格。
常見的編程范式有:
命令式編程 面向過程編程 C 面向?qū)ο缶幊?/section> C++、C#、Java 聲明式編程 函數(shù)式編程 Haskell
命令式編程
命令式編程?是使用最廣的一種編程風格,它是站在計算機的角度去思考問題,主要思想是?關(guān)注計算機執(zhí)行的步驟,即一步一步告訴計算機先做什么再做什么。
由于存在很多需要控制的步驟,所以命令式編程普遍存在以下特點:
控制語句 循環(huán)語句:while、for 條件分支語句:if else、switch 無條件分支語句:return、break、continue 變量 賦值語句
根據(jù)這些特點,我們來分析一個命令式編程案例:
//?需求:篩選出數(shù)組中為奇數(shù)的子集合
const?array?=?[1,?2,?3,?4,?5,?6,?7,?8,?9];
//?步驟1:定義執(zhí)行結(jié)果變量
let?reult?=?[];
//?步驟2:控制程序循環(huán)調(diào)用
for?(let?i?=?0;?i???//?步驟3:判斷篩選條件
??if?(array[i]?%?2?!==?0)?{
????//?步驟4:加入執(zhí)行結(jié)果
????reult.push(array[i]);
??}
}
//?步驟5:得到最終的結(jié)果 result
以上代碼通過 5 個步驟,實現(xiàn)了數(shù)組的篩選,這并沒有什么問題,但細心的同學可能會感到疑惑:這樣寫的代碼量太長了,而且并不語義化,只有閱讀完每一行的代碼,才知道具體執(zhí)行的是什么邏輯。
沒錯,這就是命令式編程的典型特點,除此之外,還有以下幾點:
命令式編程的每一個步驟都可以由程序員定義,這樣可以更精細化、更嚴謹?shù)乜刂拼a,從而提高程序的性能。 命令式編程的每一個步驟都可以記錄中間結(jié)果,方便調(diào)試代碼。 命令式編程需要大量的流程控制語句,在處理多線程狀態(tài)同步問題時,容易造成邏輯混亂,通常需要加鎖來解決。
聲明式編程
聲明式編程?同樣是一種編程風格,它通過定義具體的規(guī)則,以便系統(tǒng)底層可以自動實現(xiàn)具體功能。主要思想是?告訴計算機應(yīng)該做什么,但不指定具體要怎么做。
由于需要定義規(guī)則來表達含義,所以聲明式編程普遍存在以下特點:
代碼更加語義化,便于理解。 代碼量更少。 不需要流程控制代碼,如:for、while、if 等。
接下來,我們將上文中的數(shù)組篩選,用聲明式的方式重構(gòu)一下:
//?篩選出數(shù)組中為奇數(shù)的子集合
const?array?=?[1,?2,?3,?4,?5,?6,?7,?8,?9];
const?reult?=?array.filter((item)?=>?item?%?2?!==?0);
可以看到,聲明式編程沒有冗余的操作步驟,代碼量非常少,并且非常語義化,當我們讀到 filter 的時候,自然而然就知道是在做篩選。
我們再看一個案例:
#?使用?sql?語句,查詢?id?為?25?的學生
select?*?from?students?where?id=25
在上述代碼中,我們只是告訴計算機,我想查找 id 為 25 的同學,計算機就能給我們返回對應(yīng)的數(shù)據(jù)了,至于是怎么查找出來的,我們并不需要關(guān)心,只要結(jié)果是正確的即可。
除了上述例子之外,還有很多聲明式編程的案例:
html 用來聲明了網(wǎng)頁的內(nèi)容。 css 用來聲明網(wǎng)頁中元素的外觀。 正則表達式,聲明匹配的規(guī)則。
有了以上幾個案例,我們來總結(jié)一下聲明式編程的優(yōu)缺點:
聲明式編程不需要編寫復雜的操作步驟,可以大大減少開發(fā)者的工作量。 聲明式編程的具體操作都是底層統(tǒng)一管理,可以降低重復工作。 聲明式編程底層實現(xiàn)的邏輯并不可控,不適合做更精細化的優(yōu)化。
函數(shù)式編程
函數(shù)式編程?屬于聲明式編程中的一種,它的主要思想是?將計算機運算看作為函數(shù)的計算,也就是把程序問題抽象成數(shù)學問題去解決。
函數(shù)式編程中,我們可以充分利用數(shù)學公式來解決問題。也就是說,任何問題都可以通過函數(shù)(加減乘除)和數(shù)學定律(交換律、結(jié)合律等),一步一步計算,最終得到答案。
函數(shù)式編程中,所有的變量都是唯一的值,就像是數(shù)學中的代數(shù) x、y,它們要么還未解出來,要么已經(jīng)被解出為固定值,所以對于:x=x+1?這樣的自增是不合法的,因為修改了代數(shù)值,不符合數(shù)學邏輯。
除此之外,嚴格意義上的函數(shù)式編程也不包括循環(huán)、條件判斷等控制語句,如果需要條件判斷,可以使用三元運算符代替。
文章開頭我們提到了 webpack-chain,我們一起來看一下:
//?使用 webpack-chain 來編寫 webpack 配置。
const?Config?=?require('webpack-chain');
const?config?=?new?Config();
config.
????.entry('index')
????????.add('src/index.js')
????????.end()
????.output
?????????.path('dist')
?????????filename('my-first-webpack.bundle.js');
config.module
????.rule('compile')
????????.test(/\.js$/)
????????.use('babel')
????????.loader('babel-loader')
module.exports?=?config;
可以看到,webpack-chain 可以通過鏈式的函數(shù) api 來創(chuàng)建和修改 webpack 配置,從而更方便地創(chuàng)建和修改 webpack 配置。試想一下,如果一份 webpack 配置需要用于多個項目,但每個項目又有一些細微的不同配置,這個應(yīng)該怎么處理呢?
如果使用 webpack-chain 去修改配置,一個函數(shù) api 就搞定了,而使用命令式的編程,則需要去逐步遍歷整個 webpack 配置文件,找出需要修改的點,才能進行修改,這無疑就大大減少了我們的工作量。
函數(shù)式編程的特點
根據(jù)維基百科權(quán)威定義,函數(shù)式編程有以下幾個特點:
函數(shù)是一等公民 函數(shù)可以和變量一樣,可以賦值給其他變量,也可以作為參數(shù),傳入一個函數(shù),或者作為別的函數(shù)返回值。 只用表達式,不用語句: 表達式是一段單純的運算過程,總是有返回值。 語句是執(zhí)行某種操作,沒有返回值。 也就是說,函數(shù)式編程中的每一步都是單純的運算,而且都有返回值。 無副作用 不會產(chǎn)生除運算以外的其他結(jié)果。 同一個輸入永遠得到同一個數(shù)據(jù)。 不可變性 不修改變量,返回一個新的值。 引用透明 函數(shù)的運行不依賴于外部變量,只依賴于輸入的參數(shù)。
以上的特點都是函數(shù)式編程的核心,基于這些特點,又衍生出了許多應(yīng)用場景:
純函數(shù):同樣的輸入得到同樣的輸出,無副作用。 函數(shù)組合:將多個依次調(diào)用的函數(shù),組合成一個大函數(shù),簡化操作步驟。 高階函數(shù):可以加工函數(shù)的函數(shù),接收一個或多個函數(shù)作為輸入、輸出一個函數(shù)。 閉包:函數(shù)作用域嵌套,實現(xiàn)的不同作用域變量共享。 柯里化:將一個多參數(shù)函數(shù)轉(zhuǎn)化為多個嵌套的單參數(shù)函數(shù)。 偏函數(shù):緩存一部分參數(shù),然后讓另一些參數(shù)在使用時傳入。 惰性求值:預先定義多個操作,但不立即求值,在需要使用值時才去求值,可以避免不必要的求值,提升性能。 遞歸:控制函數(shù)循環(huán)調(diào)用的一種方式。 尾遞歸:避免多層級函數(shù)嵌套導致的內(nèi)存溢出的優(yōu)化。 鏈式調(diào)用:讓代碼更加優(yōu)雅。
這些應(yīng)用場景都大量存在于我們的日常工作中,接下來我們通過幾個案例來實戰(zhàn)一下。
函數(shù)式編程常見案例
基于函數(shù)式編程的應(yīng)用場景,我們來實現(xiàn)幾個具體的案例。
函數(shù)組合 柯里化 偏函數(shù) 高階函數(shù) 尾遞歸 鏈式調(diào)用
1、函數(shù)組合,組合多個函數(shù)步驟。
function?compose(f,?g)?{
??return?function?()?{
????return?f.call(this,?g.apply(this,?arguments));
??};
}
function?toLocaleUpperCase(str)?{
??return?str.toLocaleUpperCase();
}
function?toSigh(str)?{
??return?str?+?"!";
}
//?將多個函數(shù)按照先后執(zhí)行順序組合成一個函數(shù),簡化多個調(diào)用步驟。
const?composedFn?=?compose(toSigh,?toLocaleUpperCase);
console.log("函數(shù)組合:",?composedFn("msx"));
//?函數(shù)組合:MSX!
2、柯里化,將一個多參數(shù)函數(shù)轉(zhuǎn)化為多個嵌套的單參數(shù)函數(shù)。
//?柯里化
function?curry(targetfn)?{
??var?numOfArgs?=?targetfn.length;
??return?function?fn(...rest)?{
????if?(rest.length???????return?fn.bind(null,?...rest);
????}?else?{
??????return?targetfn.apply(null,?rest);
????}
??};
}
//?加法函數(shù)
function?add(a,?b,?c,?d)?{
??return?a?+?b?+?c?+?d;
}
//?將一個多參數(shù)函數(shù)轉(zhuǎn)化為多個嵌套的單參數(shù)函數(shù)
console.log("柯里化:",?curry(add)(1)(2)(3)(4));
//?柯里化:10
3、偏函數(shù),緩存一部分參數(shù),然后讓另一些參數(shù)在使用時傳入。
//?偏函數(shù)
function?isTypeX(type)?{
??return?function?(obj)?{
????return?Object.prototype.toString.call(obj)?===?`[object?${type}]`;
??};
}
//?緩存一部分參數(shù),然后讓另一些參數(shù)在使用時傳入。
const?isObject?=?isTypeX("Object");
const?isNumber?=?isTypeX("Number");
console.log("偏函數(shù)測試:",?isObject({?a:?1?},?123));?//?true
console.log("偏函數(shù)測試:",?isNumber(1));?//?true
4、惰性求值,預先定義多個操作,但不立即求值,在需要使用值時才去求值,可以避免不必要的求值,提升性能。
//?這里使用?C#?中的?LINQ?來演示
//?假設(shè)數(shù)據(jù)庫中有這樣一段數(shù)據(jù)?db.Gems?[4,15,20,7,3,13,2,20];
var?q?=
????db.Gems
????.Select(c?=>?c?10)
???.Take(3)
???//?只要不調(diào)用?ToList?就不會求值
???//?在具體求值的時候,會將預先定義的方法進行優(yōu)化整合,以產(chǎn)生一個最優(yōu)的解決方案,才會去求值。
????.ToList();
上述代碼中,傳統(tǒng)的求值會遍歷 2 次,第一次遍歷整個數(shù)組(8 項),篩選出小于 10 的項,輸出?[4,7,3,2],第二次遍歷這個數(shù)組(4 項),輸出?[4,7,3]。
如果使用惰性求值,則會將預先定義的所有操作放在一起進行判斷,所以只需要遍歷 1 次就可以了。在遍歷的同時判斷?是否小于 10?和?小于 10 的個數(shù)是否為 3,當遍歷到第 5 項時,就能輸出?[4,7,3]。
相比傳統(tǒng)求值遍歷的 8+4=12 項,使用惰性求值則只需遍歷 5 項,程序的運行效率也就自然而然地得到了提升。
5、高階函數(shù),可以加工函數(shù)的函數(shù)(接收一個或多個函數(shù)作為輸入、輸出一個函數(shù))。
// React 組件中,將一個組件,封裝為帶默認背景色的新組件。
//?styled-components?就是這個原理
function?withBackgroundRedColor?(wrapedComponent)?{
??return?class?extends?Component?{
????render?()?{
??????return?(<div?style={backgroundColor:?'red}?>
?????????????????<wrapedComponent?{...this.props}?/>
?????????????div>)
????}
??}
}
6、遞歸和尾遞歸。
//?普通遞歸,控制函數(shù)循環(huán)調(diào)用的一種方式。
function?fibonacci(n)?{
??if?(n?===?0)?{
????return?0;
??}
??if?(n?===?1)?{
????return?1;
??}
??return?fibonacci(n?-?1)?+?fibonacci(n?-?2);
}
console.log("沒有使用尾遞歸,導致棧溢出",?fibonacci(100));
//?尾遞歸,避免多層級函數(shù)嵌套導致的內(nèi)存溢出的優(yōu)化。
function?fibonacci2(n,?result,?preValue)?{
??if?(n?==?0)?{
????return?result;
??}
??return?fibonacci2(n?-?1,?preValue,?result?+?preValue);
}
//?result?=?0,?preValue?=?1
console.log("使用了尾遞歸,不會棧溢出",?fibonacci2(100,?0,?1));
6、鏈式調(diào)用
// lodash 中,一個方法調(diào)用完成之后,可以繼續(xù)鏈式調(diào)用其他的方法。
var?users?=?[
??{?user:?"barney",?age:?36?},
??{?user:?"fred",?age:?40?},
??{?user:?"pebbles",?age:?1?},
];
var?youngest?=?_.chain(users)
??.sortBy("age")
??.map(function?(o)?{
????return?o.user?+?"?is?"?+?o.age;
??})
??.head()
??.value();
//?=>?'pebbles?is?1'在上面,我們討論了常用的函數(shù)式編程案例,接下來我們來探究一下什么是 Monad?
在函數(shù)式編程中,Monad 是一種結(jié)構(gòu)化程序的抽象,我們通過三個部分來理解一下。
Monad 定義 Monad 使用場景 Monad 一句話解釋
Monad 定義
根據(jù)維基百科的定義,Monad 由以下三個部分組成:
一個類型構(gòu)造函數(shù)(M),可以構(gòu)建出一元類型? M。一個類型轉(zhuǎn)換函數(shù)(return or unit),能夠把一個原始值裝進 M 中。 unit(x) :? T -> M T一個組合函數(shù) bind,能夠把 M 實例中的值取出來,放入一個函數(shù)中去執(zhí)行,最終得到一個新的 M 實例。 M?執(zhí)行?T-> M?生成?M
除此之外,它還遵守一些規(guī)則:
單位元規(guī)則,通常由 unit 函數(shù)去實現(xiàn)。 結(jié)合律規(guī)則,通常由 bind 函數(shù)去實現(xiàn)。
單位元:是集合里的一種特別的元素,與該集合里的二元運算有關(guān)。當單位元和其他元素結(jié)合時,并不會改變那些元素。
乘法的單位元就是 1,任何數(shù) x 1 = 任何數(shù)本身、1 x 任何數(shù) = 任何數(shù)本身。
加法的單位元就是 0,任何數(shù) + 0 = 任何數(shù)本身、0 + 任何數(shù) = 任何數(shù)本身。
這些定義很抽象,我們用一段 js 代碼來模擬一下。
class?Monad?{
??value?=?"";
??//?構(gòu)造函數(shù)
??constructor(value)?{
????this.value?=?value;
??}
??//?unit,把值裝入?Monad?構(gòu)造函數(shù)中
??unit(value)?{
????this.value?=?value;
??}
??//?bind,把值轉(zhuǎn)換成一個新的?Monad
??bind(fn)?{
????return?fn(this.value);
??}
}
//?滿足?x->?M(x)?格式的函數(shù)
function?add1(x)?{
??return?new?Monad(x?+?1);
}
//?滿足?x->?M(x)?格式的函數(shù)
function?square(x)?{
??return?new?Monad(x?*?x);
}
//?接下來,我們就能進行鏈式調(diào)用了
const?a?=?new?Monad(2)
?????.bind(square)
?????.bind(add1);
?????//...
console.log(a.value?===?5);?//?true
上述代碼就是一個最基本的 Monad,它將程序的多個步驟抽離成線性的流,通過 bind 方法對數(shù)據(jù)流進行加工處理,最終得到我們想要的結(jié)果。
Ok,我們已經(jīng)明白了 Monad 的內(nèi)部結(jié)構(gòu),接下來,我們再看一下 Monad 的使用場景。
Monad 使用場景
通過 Monad 的規(guī)則,衍生出了許多使用場景。
組裝多個函數(shù),實現(xiàn)鏈式操作。 鏈式操作可以消除中間狀態(tài),實現(xiàn) Pointfree 風格。 鏈式操作也能避免多層函數(shù)嵌套問題? fn1(fn2(fn3()))。如果你用過 rxjs,就能體會到鏈式操作帶來的快樂。 處理副作用。 包裹異步 IO 等副作用函數(shù),放在最后一步執(zhí)行。
還記得 Jquery 時代的 ajax 操作嗎?
$.ajax({
??type:?"get",
??url:?"request1",
??success:?function?(response1)?{
????$.ajax({
??????type:?"get",
??????url:?"request2",
??????success:?function?(response2)?{
????????$.ajax({
??????????type:?"get",
??????????url:?"request3",
??????????success:?function?(response3)?{
????????????console.log(response3);?//?得到最終結(jié)果
??????????},
????????});
??????},
????});
??},
});
上述代碼中,我們通過回調(diào)函數(shù),串行執(zhí)行了 3 個 ajax 操作,但同樣也生成了 3 層代碼嵌套,這樣的代碼不僅難以閱讀,也不利于日后維護。
Promise 的出現(xiàn),解決了上述問題。
fetch("request1")
??.then((response1)?=>?{
????return?fetch("request2");
??})
??.then((response2)?=>?{
????return?fetch("request3");
??})
??.then((response3)?=>?{
????console.log(response3);?//?得到最終結(jié)果
??});
我們通過 Promise,將多個步驟封裝到多個 then 方法中去執(zhí)行,不僅消除了多層代碼嵌套問題,而且也讓代碼劃分更加自然,大大提高了代碼的可維護性。
想一想,為什么 Promise 可以不斷執(zhí)行 then 方法?
其實,Promise 和 Monad 很類似,它滿足了多條 Monad 規(guī)則。
Promise 本身就是一個構(gòu)造函數(shù)。 Monad 中的 unit,在 Promise 中可以看為: x => Promise.resolve(x)Monad 中的 bind,在 Promise 中可以看為: Promise.prototype.then
我們用代碼來驗證一下。
//?首先定義 2 個異步處理函數(shù)。
//?延遲?1s?然后?加一
function?delayAdd1(x)?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve(x?+?1);
????});
??},?1000);
}
//?延遲?1s?然后?求平方
function?delaySquare(x)?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve(x?*?x);
????});
??},?1000);
}
/****************************************************************************************/
//?單位元 e 規(guī)則,滿足:e*a = a*e = a
const?promiseA?=?Promise.resolve(2).then(delayAdd1);
const?promiseB?=?delayAdd1(2);
// promiseA === promiseB,故 promise 滿足左單位元。
const?promiseC?=?Promise.resolve(2);
const?promiseD?=?a.then(Promise.resolve);
// promiseC === promiseD,故 promise 滿足右單位元。
// promise 既滿足左單位元,又滿足右單位元,故 Promise 滿足單位元。
// ps:但一些特殊的情況不滿足該定義,下文中會講到
/****************************************************************************************/
//?結(jié)合律規(guī)則:(a * b)* c = a *(b * c)
const?promiseE?=?Promise.resolve(2);
const?promiseF?=?promiseE.then(delayAdd1).then(delaySquare);
const?promiseG?=?promiseE.then(function?(x)?{
??return?delayAdd1(x).then(g);
});
// promiseF === promiseG,故 Promise 是滿足結(jié)合律。
// ps:但一些特殊的情況不滿足該定義,下文中會講到
看完上面的代碼,不禁感覺很驚訝,Promise 和 Monad 也太像了吧,不僅可以實現(xiàn)鏈式操作,也滿足單位元和結(jié)合律,難道 Promise 就是一個 Monad?
其實不然,Promise 并不完全滿足 Monad:
Promise.resolve 如果傳入一個 Promise 對象,會等待傳入的 Promise 執(zhí)行,并將執(zhí)行結(jié)果作為外層 Promise 的值。 Promise.resolve 在處理 thenable 對象時,同樣不會直接返回該對象,會將對象中的 then 方法當做一個 Promise 等待結(jié)果,并作為外層 Promise 的值。
如果是這兩種情況,那就無法滿足 Monad 規(guī)則。
//?Promise.resolve?傳入一個?Promise?對象
const?functionA?=?function?(p)?{
??//?這時?p?===?1
??return?p.then((n)?=>?n?*?2);
};
const?promiseA?=?Promise.resolve(1);
Promise.resolve(promiseA).then(functionA);
//?RejectedPromise?TypeError:?p.then?is?not?a?function
//?由于 Promise.resolve 對傳入的 Promise 進行了處理,導致直接運行報錯。違背了單位元和結(jié)合律。
//?Promise.resolve?傳入一個?thenable?對象
const?functionB?=?function?(p)?{
??//?這時?p?===?1
??alert(p);
??return?p.then((n)?=>?n?*?2);
};
const?obj?=?{
??then(r)?{
????r(1);
??},
};
const?promiseB?=?Promise.resolve(obj);
Promise.resolve(promiseB).then(functionB);
//?RejectedPromise?TypeError:?p.then?is?not?a?function
//?由于 Promise.resolve 對傳入的 thenable 進行了處理,導致直接運行報錯。違背了單位元和結(jié)合律。
看到這里,相信大家對 Promise 也有了一層新的了解,正是借助了 Monad 一樣的鏈式操作,才使 Promise 廣泛應(yīng)用在了前端異步代碼中,你是否也和我一樣,對 Monad 充滿了好感?
Monad 處理副作用
接下來,我們再看一個常見的問題:為什么 Monad 適合處理副作用?
ps:這里說的副作用,指的是違反純函數(shù)原則的操作,我們應(yīng)該盡可能避免這些操作,或者把這些操作放在最后去執(zhí)行。
例如:
var?fs?=?require("fs");
//?純函數(shù),傳入?filename,返回?Monad?對象
var?readFile?=?function?(filename)?{
??//?副作用函數(shù):讀取文件
??const?readFileFn?=?()?=>?{
????return?fs.readFileSync(filename,?"utf-8");
??};
??return?new?Monad(readFileFn);
};
//?純函數(shù),傳入?x,返回?Monad?對象
var?print?=?function?(x)?{
??//?副作用函數(shù):打印日志
??const?logFn?=?()?=>?{
????console.log(x);
????return?x;
??};
??return?new?Monad(logFn);
};
//?純函數(shù),傳入?x,返回?Monad?對象
var?tail?=?function?(x)?{
??//?副作用函數(shù):返回最后一行的數(shù)據(jù)
??const?tailFn?=?()?=>?{
????return?x[x.length?-?1];
??};
??return?new?Monad(tailFn);
};
//?鏈式操作文件
const?monad?=?readFile("./xxx.txt").bind(tail).bind(print);
//?執(zhí)行到這里,整個操作都是純的,因為副作用函數(shù)一直被包裹在?Monad?里,并沒有執(zhí)行
monad.value();?//?執(zhí)行副作用函數(shù)
上面代碼中,我們將副作用函數(shù)封裝到 Monad 里,以保證純函數(shù)的優(yōu)良特性,巧妙地化解了副作用存在的安全隱患。
Ok,到這里為止,本文的主要內(nèi)容就已經(jīng)分享完了,但在學習 Monad 中的某一天,突然發(fā)現(xiàn)有人用一句話就解釋清楚了 Monad,自嘆不如,簡直太厲害了,我們一起來看一下吧!
Warning:下文的內(nèi)容偏數(shù)學理論,不感興趣的同學跳過即可。
Monad 一句話解釋
早在 10 多年前,Philip Wadler 就對 Monad 做了一句話的總結(jié)。
原文:A monad is a monoid in the category of endofunctors。
翻譯:Monad 是一個?自函子?范疇?上的?幺半群” 。
這里標注了 3 個重要的概念:自函子、范疇、幺半群,這些都是數(shù)學知識,我們分開理解一下。
什么是范疇?
任何事物都是對象,大量的對象結(jié)合起來就形成了集合,對象和對象之間存在一個或多個聯(lián)系,任何一個聯(lián)系就叫做態(tài)射。
一堆對象,以及對象之間的所有態(tài)射所構(gòu)成的一種代數(shù)結(jié)構(gòu),便稱之為?范疇。
什么是函子?
我們將范疇與范疇之間的映射稱之為?函子。映射是一種特殊的態(tài)射,所以函子也是一種態(tài)射。
什么是自函子?
自函子就是一個將范疇映射到自身的函子。
什么是幺半群 Monoid?
幺半群是一個存在 單位元 的半群。
什么是半群?
如果一個集合,滿足結(jié)合律,那么就是一個半群。
什么是單位元?
單位元是集合里的一種特別的元素,與該集合里的二元運算有關(guān)。當單位元和其他元素結(jié)合時,并不會改變那些元素。
如:
任何一個數(shù) + 0 = 這個數(shù)本身。那么 0 就是單位元(加法單位元)
任何一個數(shù) * 1 = 這個數(shù)本身。那么 1 就是單位元(乘法單位元)
Ok,我們已經(jīng)了解了所有應(yīng)該掌握的專業(yè)術(shù)語,那就簡單串解一下這段解釋吧:
一個?自函子?范疇?上的?幺半群?,可以理解為,在一個滿足結(jié)合律和單位元規(guī)則的集合中,存在一個映射關(guān)系,這個映射關(guān)系可以把集合中的元素映射成當前集合自身的元素。
相信掌握了這些理論知識,肯定會對 Monad 有一個更加深入的理解。
總結(jié)
本文從 Monad 的維基百科開始,逐步介紹了 Monad 的內(nèi)部結(jié)構(gòu)以及實現(xiàn)原理,并通過 Promise 驗證了 Monad 在實戰(zhàn)中發(fā)揮的重大作用。
文中包含了許多數(shù)學定義、函數(shù)式編程的理論等知識,大多是參考網(wǎng)絡(luò)資料和自我經(jīng)驗得出的,如果有錯誤的地方,還望大家多多指點 ?
最后,如果你對此有任何想法,歡迎留言評論!
●?你不知道的前端項目自動化部署(實戰(zhàn)教學,超詳細教程)
·END·
匯聚精彩的免費實戰(zhàn)教程
喜歡本文,點個“在看”告訴我


