正文
一退子、什么是函數(shù)式編程?
我的理解是函數(shù)式編程就是一種抽象程度很高的編程范式切端,純粹的函數(shù)式編程語言編寫的函數(shù)可以看成沒有變量抬探,因此,任意一個函數(shù)帆赢,只要輸入是確定的小压,輸出就是確定的。.函數(shù)式編程不是用函數(shù)來編程椰于,也不是傳統(tǒng)的面向過程編程怠益。主旨在于將復雜的函數(shù)符合成簡單的函數(shù)(計算理論,或者遞歸論瘾婿,或者拉姆達演算)蜻牢。運算過程盡量寫成一系列嵌套的函數(shù)調(diào)用。函數(shù)式編程我覺得更貼近于數(shù)學思想或者說計算偏陪。
二抢呆、函數(shù)式編程的特性
1.函數(shù)是"第一等公民"
所謂"第一等公民"(first class),指的是函數(shù)與其他數(shù)據(jù)類型一樣笛谦,處于平等地位抱虐,可以賦值給其他變量,也可以作為參數(shù)饥脑,傳入另一個函數(shù)恳邀,或者作為別的函數(shù)的返回值。
舉例來說灶轰,下面代碼中的print變量就是一個函數(shù)谣沸,可以作為另一個函數(shù)的參數(shù)。
var print = function(i){ console.log(i);};
[1,2,3].forEach(print);
2.只用”表達式"笋颤,不用"語句"
"表達式"(expression)是一個單純的運算過程乳附,總是有返回值;"語句"(statement)是執(zhí)行某種操作伴澄,沒有返回值赋除。函數(shù)式編程要求,只使用表達式秉版,不使用語句贤重。也就是說茬祷,每一步都是單純的運算清焕,而且都有返回值。
原因是函數(shù)式編程的開發(fā)動機,一開始就是為了處理運算(computation)秸妥,不考慮系統(tǒng)的讀寫(I/O)滚停。"語句"屬于對系統(tǒng)的讀寫操作,所以就被排斥在外粥惧。
當然键畴,實際應用中,不做I/O是不可能的突雪。因此起惕,編程過程中,函數(shù)式編程只要求把I/O限制到最小咏删,不要有不必要的讀寫行為惹想,保持計算過程的單純性。
3.沒有"副作用"
所謂"副作用"(side effect)督函,指的是函數(shù)內(nèi)部與外部互動(最典型的情況嘀粱,就是修改全局變量的值),產(chǎn)生運算以外的其他結(jié)果辰狡。
函數(shù)式編程強調(diào)沒有"副作用"锋叨,意味著函數(shù)要保持獨立,所有功能就是返回一個新的值宛篇,沒有其他行為娃磺,尤其是不得修改外部變量的值。
4.不修改狀態(tài)
在函數(shù)式編程中叫倍,我們通常理解的變量在函數(shù)式編程中也被函數(shù)代替了:在函數(shù)式編程中變量僅僅代表某個表達式豌鸡。這里所說的’變量’是不能被修改的。所有的變量只能被賦一次初值段标。(下面會詳細介紹變量被函數(shù)代替)
5.引用透明性
函數(shù)程序通常還加強引用透明性涯冠,即如果提供同樣的輸入,那么函數(shù)總是返回同樣的結(jié)果逼庞。就是說蛇更,表達式的值不依賴于可以改變值的全局狀態(tài)。
三赛糟、函數(shù)式編程常用核心概念和技術(shù)
1.純函數(shù)
什么是純函數(shù)呢派任?
對于相同的輸入,永遠會得到相同的輸出璧南,而且沒有任何可觀察的副作用掌逛,也不依賴外部環(huán)境的狀態(tài)的函數(shù),叫做純函數(shù)司倚。
舉個栗子:
var xs = [1,2,3,4,5];// Array.slice是純函數(shù)豆混,因為它沒有副作用篓像,對于固定的輸入,輸出總是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice會對原array造成影響皿伺,所以不純
xs.splice(0,3);
2.函數(shù)柯里化(高級函數(shù))
高階函數(shù)员辩,就是把函數(shù)當參數(shù),把傳入的函數(shù)做一個封裝鸵鸥,然后返回這個封裝函數(shù),達到更高程度的抽象奠滑。
我的理解:傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個函數(shù)去處理剩下的參數(shù)妒穴。
看一個例子:
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
柯里化可以理解為是一種“預加載”函數(shù)的方法宋税,通過傳遞較少的參數(shù),得到一個已經(jīng)記住了這些參數(shù)的新函數(shù)讼油,某種意義上講弃甥,這是一種對參數(shù)的“緩存”,是一種非常高效的編寫函數(shù)的方法汁讼。
3.函數(shù)組合
為了解決函數(shù)嵌套過深淆攻,洋蔥代碼:h(g(f(x))),我們需要用到“函數(shù)組合”嘿架,我們可以
來用柯里化來改他瓶珊,讓多個函數(shù)像拼積木一樣。
函數(shù)編程的函數(shù)組合:兩個純函數(shù)組合之后返回了一個新函數(shù),舉個例子:
const compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
const toUpperCase = function(x) {
return x.toUpperCase();
};
const exclaim = function(x) {
return x + "!";
};
const shout = compose( exclaim , toUpperCase );
console.log(shout("hello world")); //HELLO WORLD!
可以對他簡化一下:
const compose=(f,g)=>(x=>f(g(x)))
const toUpperCase=x=>x.toUpperCase()
const exclaim=x=>x+'!'
const shout=compose(exclaim,toUpperCase)
console.log(shout('hello world')); //HELLO WORLD!
compose中的參數(shù)的順序是隨意的耸彪,這就類似于乘法中的交換律 xy=yx**,
所以函數(shù)式編程貼近于數(shù)學計算伞芹。
4.遞歸與尾遞歸
指函數(shù)內(nèi)部的最后一個動作是函數(shù)調(diào)用。 該調(diào)用的返回值蝉娜, 直接返回給函數(shù)唱较。 函數(shù)調(diào)用自身, 稱為遞歸召川。 如果尾調(diào)用自身南缓, 就稱為尾遞歸荧呐。 遞歸需要保存大量的調(diào)用記錄汉形, 很容易發(fā)生棧溢出錯誤, 如果使用尾遞歸優(yōu)化倍阐, 將遞歸變?yōu)檠h(huán), 那么只需要保存一個調(diào)用記錄岔冀, 這樣就不會發(fā)生棧溢出錯誤了。通俗點說使套,尾遞歸最后一步需要調(diào)用自身,并且之后不能有其他額外操作童漩。
舉個例子:我的理解為滿足遞歸條件相當于捕魚時先撒網(wǎng),等達到遞歸邊界即捕到魚時侧馅,再返回自身及收網(wǎng);尾遞歸就為當捕到魚時魚已經(jīng)到你手里了罗晕,具體舉個例子:
// 不是尾遞歸赠堵,無法優(yōu)化
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
} //這里的遞歸當?shù)竭_邊界條件時會把值返回給上一個調(diào)用它的函數(shù)
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
} //ES6強制使用尾遞歸
// 這里的尾遞歸就是當?shù)降走吔鐥l件時直接將結(jié)果 return 出來
我們看一下遞歸和尾遞歸執(zhí)行過程:
遞歸:
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1);
}
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15 //遞歸非常消耗內(nèi)存,因為需要同時保存很多的調(diào)用幀揍愁,這樣,就很容易發(fā)生“棧溢出”
尾遞歸:
function sum(x, total) {
if (x === 1) {
return x + total;
}
return sum(x - 1, x + total);
}
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15
整個計算過程是線性的,調(diào)用一次sum(x, total)后怯屉,會進入下一個棧,相關(guān)的數(shù)據(jù)信息和跟隨進入羡儿,不再放在堆棧上保存缅叠。當計算完最后的值之后,直接返回到最上層的sum(5,0)领曼。這能有效的防止堆棧溢出。 在ECMAScript 6单刁,我們將迎來尾遞歸優(yōu)化,通過尾遞歸優(yōu)化褥傍,javascript代碼在解釋成機器碼的時候,將會向while看起朋贬,也就是說,同時擁有數(shù)學表達能力和while的效能糠亩。
5.惰性計算(求值)
在惰性計算中,表達式不是在綁定到變量時立即計算垂寥,而是在求值程序需要產(chǎn)生表達式的值時進行計算。延遲的計算使你可以編寫可能潛在地生成無窮輸出的函數(shù)过椎。因為不會計算多于程序的其余部分所需要的值,所以不需要擔心由無窮計算所導致的 out-of-memory 錯誤灰嫉。簡單的來說就是:
按需索取,能不多做事股耽,絕不多做
這里有自己實現(xiàn)的簡單的惰性求值
用JavaScript寫一個惰性求值的最簡實現(xiàn)
惰性求值官方文檔
官方文檔
四物蝙、函數(shù)式編程總結(jié)
1.函數(shù)式編程中的每個符號都是 const 的,于是沒有什么函數(shù)會有副作用震嫉。誰也不能在運行時修改任何東西,也沒有函數(shù)可以修改在它的作用域之外修改什么值給其他函數(shù)繼續(xù)使用悴势。這意味著決定函數(shù)執(zhí)行結(jié)果的唯一因素就是它的返回值侥加,而影響其返回值的唯一因素就是它的參數(shù)。
2.函數(shù)式編程不需要考慮”死鎖"(deadlock)矗蕊,因為它不修改變量朋魔,所以根本不存在"鎖"線程的問題。不必擔心一個線程的數(shù)據(jù),被另一個線程修改镶奉,所以可以很放心地把工作分攤到多個線程,部署"并發(fā)編程"(concurrency)。
3.函數(shù)式編程中所有狀態(tài)就是傳給函數(shù)的參數(shù)亿蒸,而參數(shù)都是儲存在棧上的。這一特性讓軟件的熱部署變得十分簡單。只要比較一下正在運行的代碼以及新的代碼獲得一個diff灰蛙,然后用這個diff更新現(xiàn)有的代碼宣旱,新代碼的熱部署就完成了耗溜。