淺談 Function Programing 編程範式
來自團隊 「史曉宇」 同學的分享。
Tecvan All or nothing, now or never 👉
背景
設想一個場景,假如需要實現這樣兩個函數:
-
transform1
:input 一個字符串,output 要全部轉成大寫並尾部加感嘆號修飾; -
transform2
:input 一個字符串,output 要全部轉成小寫並尾部加感嘆號修飾。
如果按以往命令式編程思維,可能會這麼寫:
const transform1 = (str) => {
if (typeof str === "string") {
return `${str.toUpperCase()}!`;
}
return "Not a string";
};
const transform2 = (str) => {
if (typeof str === "string") {
return `${str.toLowerCase()}!`;
}
return "Not a string";
};
transform1("hello world"); // "HELLO WORLD !"
transform2("HELLO WORLD"); // "hello world !"
兩個函數雖效果不同,但代碼框架極爲相似,邏輯冗餘且僵硬,比較難實現複用。相對而言,函數式編程思維則會盡量將邏輯抽象拆解爲可被複用的若干最小單位,同樣的需求可能會這麼實現:
const { flow } = require("lodash/fp");
const toUpper = (str) => str.toUpperCase();
const toLower = (str) => str.toLowerCase();
const exclaim = (str) => `${str}!`;
const isString = (str) => (typeof str === "string" ? str : "Not a string");
const transform1 = flow(isString, toUpper, exclaim);
const transform2 = flow(isString, toLower, exclaim);
transform1("hello world"); // "HELLO WORLD !"
transform2("HELLO WORLD"); // "hello world !"
剛開始可能覺得沒什麼必要,但是在中大型項目裏尤其好用,因爲我們也不知道未來需求會變得多複雜。FP 使用大量的 Function,每個 function 都是一個單一的功能,再按功能需求以特定的方式組合起來,編寫時易於複用,在出現 bug 時也易於快速定位到相關的功能函數,使得代碼減少重複、容易理解、容易改變、容易排除錯誤和具有彈性。
核心概念
FP(Functional Programming) 是一種通過簡單地組合一組函數來編寫程序的風格,它推薦我們將幾乎所有東西都包裝在函數中,編寫大量可重用的小函數,然後簡單地一個接一個地調用它們以獲得類似的結果:( func1.func2.func3 )
或以組合方式,例如:func1(func2 (func3()))
。總而言之是:一種抽象思維、一種編程風格、一種編程規範。
FP 具有以下特點:
- Function 爲 First-class citizen(一等公民)
這個特性意味着函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作爲參數,傳入另一個函數,或者作爲別的函數的返回值,而 js 的 function 本來就有這個特性。這也是 FP 得以實現的前提。
- Declarative Programming(聲明式編程)
FP 是 Declarative Programming 的代表,邏輯爲用較爲抽象的程式碼,理解代碼想要達到怎樣的目標,Function 之間不會互相共用 state 狀態(着重 what)。而 Imperative Programming (命令式編程) 比較容易寫出狀態互相依賴的代碼(着重 how)。舉個加總 Array 🌰:
//Imperative 着重how一步步得出結果
var array = [3,2,1]
var total = 0
for( var i = 0 ; i <= array.length ; i ++ ){
total += array[i]
}
//OOP
//通過封裝把狀態(數據)和行爲(方法)內聚到類中,對外提供能夠訪問或操作狀態的方法。
class Total {
constructor(numbers = []) {
this._numbers = numbers;
}
calc() {
const numbers = this._numbers;
let totle = 0;
for (let i = 0; i < numbers.length; i++) {
totle += numbers[i]
}
return totle;
}
}
const totle1 = new Totle([1, 2, 3, 4, 5]);
console.log(totle1.calc());//15
//Declarative 只需要知道 reduce 做了什麼,不關心reduce是怎樣做的
var array = [3,2,1]
array.reduce( function( previous, current ){
return previous + current
})
- 沒有 Side Effect(副作用)
Side Effect:在完成函數主要功能之外完成的其他副要功能。會導致不易維護代碼、得到無法預期結果等等。而平常撰寫 javaScript 容易造成的 Side Effect 非常之多,例如:
- 修改外部的 state
// 改了 global 變量
var a = 0;
a++;
const list = [{type:'香蕉',age:18},...];
// 修改 list 中的 type 和 age
list.map(item => {
item.type = 1;
item.age++;
})
-
發送 HTTP Request
-
Rendering screen
-
使用會改變原數組 / 變量的 JS method (eg. splice)
-
修改任何外部變量
-
DOM 操作
-
讀取 input 的值
-
Changing DB value
-
logging & console: 改變了系統狀態
- Immutable data
所有的數據都是不可變的,這意味着如果想修改一個對象,那應該創建一個新的對象用來修改,而不是修改已有的對象。
// mutable
const balls = ['basketball', 'volleyball', 'billiards']
balls[1] = 'Table Tennis'; // 改變數組原有項
balls // ['Table Tennis', 'volleyball', 'billiards']
// immutable
const balls = ['basketball', 'volleyball', 'billiards']
const newBalls = [...balls] // 複製一份
newBalls[1] = 'Table Tennis';
balls // ['basketball', 'volleyball', 'billiards'] 跟原本一樣
newBalls// ['Table Tennis', 'volleyball', 'billiards']
- Stateless
對於一個函數,完全不依賴外部狀態的變化
// Stateful
const x = 4;
x++; // x 變 5
// 省略 100 行...
x*2 // ??x是啥都忘了
//Stateless 不用擔心x是什麼
const x = {
val: 0
};
const x1 = x => { val: x.val + 1};
- Pure Function
遵守 one input, one output 原則,不管輸入幾次同樣值,輸出結果永遠相同,且永遠有輸出值。只做運算與返回return
,而且不對外部世界造成任何改變 (沒有 Side Effect)。Pure Function 裏面 data 多是 immutable data 與 stateless 的。另外當一個函數是 pure function 且不依賴任何外部狀態只依賴函數參數,也稱作 referential transparency (引用透明)。
//impure 有side effect
const add = (x, y) => {
console.log(`Adding ${x} ${y}`)
return x + y
}
//pure
const add = (x, y) => {
return {result: x + y, log: `Adding ${x} ${y}`}
}
//impure 當 n=4 沒有返回值
function tll(i){
if(i<3){
return 0
}
if(i>5){
return 1
}
}
//pure
function tll(i){
if(i<3){
return 0
}else{
return 1
}
}
//impure 相同輸入返回值不一樣
let x = 1
const count = ()=>x++
//pure
const count = (x)=>x+1
- 柯理化拆分,**「Composition」**合成
柯理化的意義是將具有多個參數的多元函數轉化爲具有較少參數的單元函數的過程。簡單的柯理化函數是這樣的:
const curry = (fn, length = fn.length, ...args) =>
args.length >= length ? fn(...args) : curry.bind(null, fn, length, ...args);
柯理化的作用是可以固定參數,降低函數通用性,提高函數的適合用性。舉個🌰:
// 假設一個通用的請求 APIconst request = (type, url, options) => ...
// GET 請求
request('GET', 'http://....')
// POST 請求
request('POST', 'http://....')
// 但是通過柯理化,我們可以抽出特定 type 的 request
const get = request('GET');
get('http://', {..})
Composition 思維一般有兩種實現形式,一是 compose:(fa,fb,fc)=>x=>fa(fb(fc(x))),一是 pipe:(fa,fb,fc)=>x=>fc(fb(fa(x)))
compose 函數的簡單實現:
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
pipe 函數的實現同上不過改變一下執行順序而已:
function pipe(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => b(a(...args)));
}
舉一個 lodash 與 lodash/fp 的🌰介紹柯理化與 Composition 組合的意義:
//lodash實現對請求數據的處理 =>套娃(無柯理化)
const getIncompleteTaskSummaries = async function (memberName) {
let data = await fetchData();
return sortBy(
map(reject(filter(get(data, "tasks"), "username"), "complete"), (task) =>
pick(task, ["id", "dueDate", "title", "priority"])
),
"dueDate"
);
};
//lodash/fp 對FP有着更好的支持,包括完全柯理化、data-last等
const getIncompleteTaskSummaries = async function (memberName) {
let data = await fetchData();
return compose(
sortBy("dueDate"),
map(pick(["id", "dueDate", "title", "priority"])),
reject("complete"),
filter("username"),
get("tasks")
)(data);
};
可以看到經過柯理化拆分提高函數適用性後,通過函數組合使得代碼如此的流暢、簡潔。通過柯理化拆分和函數組合可以使得 FP 發揮很大的效用,也是 FP 必不可少的兩步操作,可以將柯理化後的函數比作加工站,函數組合比作流水線。
總結
lodash/fp、ramda 都具備 data-last、完全柯理化、組合函數、pure 純函數等利於 FP 的特點。但相比之下兩者也有些差異:
-
lodash/fp 依賴於 lodash,是在 lodash 基礎上實現的對函數式編程的傾斜,好上手,但是受限於 lodash,有很多侷限性。ramda 沒有前置依靠,完全 FP,整個庫貫穿 FP 思想,但是上手成本高。
-
ramda 具備很多邏輯判斷的函數(when,ifElse 等),而 lodash/fp 暫無。
-
ramda 有更友善的文檔,lodash/fp 更多要與 lodash 進行對照。
資料:
https://ramdajs.com/docs/
https://devdocs.io/lodash~4/index
https://github.com/lodash/lodash/wiki/FP-Guide
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/A1LM8bWlcI8_fgUuCISU8w