search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

月影談為什麼我們要用函數式編程

目前前端界(以及其他一些領域)對函數式編程大體上兩種態度,一些人是覺得函數式編程特牛逼,尤其是現在許多新生的框架和庫都在標榜自己的函數式特徵。而另一些人,又覺得函數式編程學起來很難,而且似乎也沒有什麼卵用,理由是在自己經歷的項目裡面很難看到具體的函數式編程應用場景,甚至其中許多人認同一個觀點,覺得函數式編程只適合於學術研究,很難在工程項目中實際使用。

不管你在閱讀本文之前屬於哪一種人,又或者你是剛接觸函數式編程的新人,都沒有關係。本文不是研究函數式編程範式的學術研究,而函數式編程作為一個可以說是程序設計理論中最古老的編程範式,在它幾十年上百年的發展歷史中,已經積累了大量的資料和素材,對於想要在學術領域裡完全弄明白它的同學,完全可以在網上、書店里找到各種資料。本文的重點不在於概念,而在於實戰。因此,你不會聽到太多各種函數式編程的名詞討論,比如諸如 Curry、Mond 之類的專業術語。相反,我們主要來討論函數式編程在前端領域內使用的一些實際例子,了解為什麼前端需要學習函數式編程,使用函數式編程寫代碼能給我們帶來什麼。如果弄明白了這些,那麼關於函數式編程不實用的謠言也就不攻自破了。

數據抽象或過程抽象

為什麼我們接受面向過程或面向對象思想很容易,而我們要完全接受函數式編程卻感覺難得多?

我認為這個問題大體上可以這麼解釋:

人腦本能地容易理解「看得見「、「摸得著」的物體,對於「運動」和「變化」一類不著形的東西,人腦理解起來要略微地費勁一些。而人類要做好一件複雜的事情,大腦有兩種抽象方向,一種是對實體進行抽象,另一種是對過程進行抽象:

簡答來說,即在軟體設計的過程中,如果要保證軟體產品的功能穩定可用,同時要保證它的靈活性和可擴展性,那麼系統就要有變化的部分和不變的部分。哪些部分應當設計成「不變」,哪些部分應當設計成「可變」,在這個取捨過程中,FP(函數式編程)和 OOP(面向對象編程)正是走了兩條不同的路線。

面向對象對數據進行抽象,將行為以對象方法的方式封裝到數據實體內部,從而降低系統的耦合度。而函數式編程,選擇對過程進行抽象,將數據以輸入輸出流的方式封裝進過程內部,從而也降低系統的耦合度。兩者雖是截然不同,然而在系統設計的目標上可以說是殊途同歸的。

面向對象思想和函數式編程思想也是不矛盾的,因為一個龐大的系統,可能既要對數據進行抽象,又要對過程進行抽象,或者一個局部適合進行數據抽象,另一個局部適合進行過程抽象,這都是可能的。數據抽象不一定以對象實體為形式,同樣過程抽象也不是說形式上必然是 functional 的,比如流式對象(InputStream、OutputStream)、Express 的 middleware,就帶有明顯的過程抽象的特徵。但是在通常情況下,OOP更適合用來做數據抽象,FP更適合用來做過程抽象。

純函數

再具體深入下去之前,我們先來解答一個問題,那就是為什麼用 FP 或過程抽象能夠降低系統的耦合度。這裡我們要先理解一個概念,這個概念叫「純函數」。

根據定義,如果一個函數符合兩個條件,它被稱為純函數:

  • 此函數在相同的輸入值時,總是產生相同的輸出。函數的輸出和當前運行環境的上下文狀態無關。

  • 此函數運行過程不影響運行環境,比如不會觸發事件、更改環境中的對象、終端輸出值等。

簡單來說,也就是當一個函數的輸出不受外部環境影響,同時也不影響外部環境時,該函數就是純函數。

JavaScript 內置函數中有不少純函數,也有不少非純函數。

比如以下函數是純函數:

  • String.prototype.toUpperCase

  • Array.prototype.map

  • Function.prototype.bind

以下函數不是純函數:

  • Math.random

  • Date.now

  • document.body.appendChild

  • Array.prototype.sort

為什麼要區分純函數和非純函數呢?因為在系統里,純函數與非純函數相比,在可測試性、可維護性、可移植性、并行計算和可擴展性方面都有著巨大的優勢。

在這裡我用可測試性來舉例:

對於純函數,因為是無狀態的,測試的時候不需要構建運行時環境,也不需要用特定的順序進行測試:

test(t = {

t.is(add(10 20) 30); //add(x,y) 是個純函數,不需要為它構建測試環境

...

});

對於非純函數,就比較複雜:

test.before(t = {

let list = document.createElement('ul');

list.id = 'xxxxxx';

...

});

test(t = {

let list = document.getElementById('xxxxxx');

t.is(sortList(list).innerHTML `<ul>

...

</ul>`);

});

test.after(t = {

...

document.removeChild(list);

});

函數式編程能夠減少系統中的非純函數

首先我們看一個例子:

//two impure functions

function setColor(el color){

el.style.color = color;

}

function setColors(els color){

els.forEach(el = setColor(el color));

}

let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');

let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');

setColors(items2 'green');

setColors(items1 'red');

在這裡我們有兩個彼此依賴的非純函數,setColor(el, color) 和 setColors(els, color)。在測試的時候,我們需要構建環境來測試兩個函數。

現在,我們用函數式編程思想來改造這個系統:

//only one impure function

function batch(fn){

return function(target ...args){

if(target.length >= 0){

return Array.from(target).map(item = fn.apply(this [item ...args]));

}else{

return fn.apply(this [target ...args]);

}

}

}

function setColor(el color){

el.style.color = color;

}

let setColors = batch(setColor);

let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');

let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');

setColors(items2 'green');

setColors(items1 'red');

在這裡,我們建立一個過程抽象的高階函數 batch(fn),這個函數的作用是,對它的輸入函數返回一個新的函數,這個函數與輸入函數的區別是,如果調用的第一個實參是一個數組,那麼將這個數組展開,用每一個值依次調用輸入函數,返回一個數組,包活每次調用返回的結果。

batch(fn) 本身雖然看似複雜,但是有意思的事,這個函數無疑是純函數,所以 batch(fn) 自身的測試是非常簡單的:

test(t = {

let add = (x y) = x + y;

let listAdd = batch(add);

t.deepEqual(listAdd([123] 1) [234]);

});

由於我們上面舉的例子 setColor 和 setColors 雖然不是純函數,但是卻非常簡單,因此似乎設計 batch(fn) 的意義不大,有把系統變得更複雜的嫌疑。然而,對於有許多操作 DOM 的函數的框架或庫,有了 batch(fn),我們就可以實現很簡單的介面(對單一元素操作),然後利用 batch(fn) 獲得更複雜介面(對元素進行批量操作),從而大大降低系統本身的複雜的,提升可維護性。

注意一點,batch(fn) 輸出的函數有副作用,然而 batch(fn) 用閉包將輸出的函數的副作用限制在了 batch(fn) 的作用域內。

Ramda.js 的 lift 方法

Ramda.js 的 lift 方法和 batch 有一點點類似,不過功能更強大。讓我們來用它實現一個有一點點「燒腦」的效果,來作為這篇文章的結尾:

async function reducer(promise action){

let res = await promise;

return action(res);

}

function continuous(...functors){

return async function(input){

return await functors.reduce(reducer input)

}

}

function sleep(ms){

return new Promise(resolve = setTimeout(resolve ms));

}

async function setColor(item color){

await sleep(500);

item.style.color = color;

}

let comb = R.lift((el color) = {

return [el color];

});

let changeColorTo = (args) = R.partial(setColor args);

let items = Array.from(list.children);

let task = R.map(changeColorTo comb(

items

['red' 'orange' 'yellow']

));

continuous(...task)(0);



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦