A programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm in which function definitions are trees of expressions that each return a value, rather than a sequence of imperative statements which change the state of the program. - wiki
看不懂維基解釋很正常,基本上 Functional Programming (之後為了方便都盡量簡稱 FP) 就是
所以 FP 不是一種 “程式語言”,也與框架無關,以 JS 來說,不管你是用 Vue、React、Angular、原生 JS 都是可以用 FP 思維去撰寫的。
學 FP 很像是自己學烘焙
雖然 FP 一開始的學習曲線真的頗高(要花許多時間理解),但是很快可以嚐到甜頭,尤其是在開發中大型專案時,不管維護或加功能都很容易。
想學一個新東西之前,一定是因為舊有方式出現一些問題所以才想用新思維 / 撰寫風格 / 工具 / 框架 解決。
大家可能都有遇過,小案子寫起來很順沒有什麼太大問題,但當系統越變越複雜,就會發現難以維護、執行 DRY (Don't Repeat Yourself) 很困難、總是花很多時間 debug、不知道怎麼寫測試,甚至出現改 A 壞 B,改 B 又壞 C 的狀態。
(圖來自 J.H. Blog)
因為一個函式包含非常多功能,所以很難共用也容易出現重覆。例如若需要寫有兩個函式, transform1
的 input 是一個字串,output 要轉成全大寫; transform2
的 input 也是一個字串,output 要轉成全小寫。
/**
@parm {string} str
@return {string} - 全大寫
*/
const transform1 = (str) => {}
/**
@parm {string} str
@return {string} - 全小寫
*/
const transform2 = (str) => {}
用以前思維大略會長以下
// Old Way
const transform1 = (str) => {
if(typeof str === 'string'){
return `${str.toUpperCase()}!`;
}
return 'Not a string'
}
const transform2 = (str) => {
if(typeof str === 'string'){
return `${str}!`;
}
return 'Not a string'
}
transform1('hello world'); // "HELLO WORLD !"
transform2('hello world'); // "hello world !"
發現很多地方都重覆了,就算有心想做 DRY ,在新增功能時也會一直需要修改到 transformX
函式。
FP 精華就是會盡量抽象化 (抽象化時命名很重要,最好是一看函式名稱就知道在做什麼) 並拆分成最小單位。一開始也許會覺得多餘,但 FP 中每一個 function 都是可以再利用的 (Reusable),在中大型專案尤其好用 (畢竟你永遠無法知道未來需求會變多複雜)
// FP
const toUpper = str => str.toUpperCase()
const exclaim = str => str + '!'
const isString = str => typeof str === 'string' ? str : 'Not a string'
let transform1 = pipe(
isString,
toUpper,
exclaim
)
let transform2 = pipe(
isString,
exclaim
);
transform1('hello world'); // "HELLO WORLD !"
transform2('hello world'); // "hello world !"
以前自己寫的 function 不是 Pure 的,也就是同樣輸入輸出的卻可能不同,所以當系統一大你也不會知道是哪個 Function 裡的哪一行導致 state 的改變
var state = ['apple', 'banana', 'cherry']
function A () {
const s = state.reverse()
return s;
}
function grabCherry () {
// ...省略 100 行
return state[2];
}
//
grabCherry() // 'cherry'
A();
grabCherry() // 'apple'
而在 FP 世界永遠秉持 "One input, one output",不管輸入幾次同樣值,回傳結果永遠相同。單就這個可預測的特性,就很好寫測試並且 debug 時也會容易很多。
一有判斷就是 if
、 while
、 for
、 switch
用到底,判斷一多就會變成難以閱讀的窘境。接手別人專案往往要花很多時間理解別人在寫什麼
function calculationBMI(bmi
if(bmi < 0) {
return '資料錯誤'
} else if(bmi < 18.5) {
return '過輕'
} else if(bmi >= 18.5 && bmi < 24) {
return '正常'
} else if(bmi >= 24 && bmi < 27) {
return '過重'
} else if(bmi >= 27 && bmi < 30) {
return '輕度肥胖'
} else if(bmi >= 30 && bmi < 35) {
return '中度肥胖'
} else{
return '重度肥胖'
}
}
calculationBMI(20); // '正常'
如果改成 FP,就算是過三個月回來看或接手別人的 code 也很了解這個函式在做什麼
// match 函式先省略
const lessThan = x => bmi => bmi< x ;
const calculationBMI = BMI =>
match(BMI)
.on(lessThan(0), () => '資料錯誤')
.on(lessThan(18.5), () => '體重過輕')
.on(lessThan(24), () => '正常')
.on(lessThan(27), () => '過重')
.on(lessThan(30), () => '輕度肥胖')
.on(lessThan(35), () => '中度肥胖')
.otherwise(() => "重度肥胖")
calculationBMI(20); // '正常'
FP 使用大量的 Function,幾乎每個 Function 都可以由更小的 Function 組合出來,例如 lessThan
,這樣好處是可以減少程式碼的重複,所以 FP 的寫法通常比較簡短跟容易。統整 FP 的好處就是
容易理解、容易改變、容易除錯和具有彈性
看完以上,有沒有讓你覺的想要學 FP 了呢?
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。
我也是 FP 愛好者,但覺得這篇文章可能有一點錯怪 OOP 了;個人覺得作者描述的問題比較偏向「程式設計」本身的問題,而非 OOP 的問題。
有心學習 OOP 的話 DRY、pure function、好讀好維護等等也都是完全辦得到的。
FP 和 OOP 的目標應該都是適度替程式設計附加抽象化,減少無謂的細節,更好閱讀及維護,溝通也會更加容易,只是 OOP 偏向更大觀點的抽象, FP 則偏向實作細節的抽象。
系統越來越龐大時,用 OOP 的觀點會很容易看出單元跟單元間的互動,但若只使用 FP 來看則會被密密麻麻的 function 互動給淹沒。
而進入實作層面時,用 FP 的觀念可以很快地把功能本身以抽象化的方式實作出來,OOP 雖然也做得到但確實就比繁瑣。
不得不說實作程式碼細節時用 FP 來寫真的很過癮 XD;但 OOP 也沒有作者提的那麼糟;以上是我自己的觀點,歡迎一起討論囉
想請教一下您說得 "程式設計" 是什麼意思
就是程式語言本身使用的問題像是 if
、for
這類的問題,使用 OOP 只要妥善設計也能大幅度消滅這些語法,但它實際還是存在只是包起來了;而使用 FP 其實也是把這些語法包裝起來使用,不搭配其他 library 一樣必免不了自己寫 if
的情境 (再怎樣包裝也要寫一次吧)。
樓下良葛格也是針對您的程式語言撰寫本身提出建議,可以參考看看囉
謝謝你們花時間給我建議,已經更新文章說法了,感謝指教
系統越來越龐大時,用 OOP 的觀點會很容易看出單元跟單元間的互動,但若只使用 FP 來看則會被密密麻麻的 function 互動給淹沒。
大大的留言讓我茅塞頓開,曾經我也想在Server中大量使用FP,但後來就被這些pure function給淹掛了XD,如果偏向以實作面
中來使用FP來抽象,那的確是很順手的思維,謝謝指教~
不建議從 OOP 的缺點來體會 FP 的好,FP 與 OOP 並不衝突,只要你能控管 OOP 的狀態,OOP 與 FP 也能很開心地結合在一起。
FP 的出發點在 immutable,這就造成了 FP 的宣告式(declarative)典範,宣告式相對的是命令式(imperative),也就是 C、Java、JavaScript 這類天生允許 mutable 的語言。
因為命令式典範中,可以輕易地變更狀態,也就造成開發者對狀態過於輕忽,將一堆狀態轉移寫在一塊程式爛泥塊中,在系統變得逐漸龐大的過程,狀態轉移越來越複雜,最後甚至根本搞不清楚狀態是如何變化的,也造成了程式碼的不易重用。
FP 一開始的出發點是 immutable,這是一種強制性,用來約束開發者,限制其不能隨心所欲改變狀態,immutable 的結果是,不會有迴圈,也就強迫改變了寫程式的方式,迫使開發者必須得分解程式,抽取為函式或方法,改為宣告式的遞迴風格等。
是不是 pure 跟是否為 OOP 沒有關係,也就是說,你也可以使用 OOP 時,設計物件為 immutable,設計方法中不能改變變數值,結果也就會強制你寫出可組合的程式碼,你的物件會是 pure,你的方法也會是 pure,因此 OOP 與 FP 並不衝突。
你的這個範例:
// OOP
const transform1 = (str) => {
if(typeof str === 'string'){
return `${str.toUpperCase()}!`;
}
return 'Not a string'
}
const transform2 = (str) => {
if(typeof str === 'string'){
return `${str}!`;
}
return 'Not a string'
}
transform1('hello world'); // "HELLO WORLD !"
transform2('hello world'); // "hello world !"
其實跟 OOP 沒有關係,函式本身就是 pure,只不過你沒有抽取出可重用的部份,也就是說,就算你從 immutable 出發,沒有能觀察出可重用部份,照樣也無法 DRY。
DRY 主要的精神是觀察重複、抽取重複,跟 OOP 或 FP 也沒有直接關係,從你的這個範例出發,觀察出重複的部份是流程,因此可重構出:
function transform(str, mapper) {
if(typeof str === 'string'){
return mapper(str);
}
return 'Not a string'
}
transform('hello world', str => `${str.toUpperCase()}!`);
transform('hello world', str => `${str}!`);
真要說 DRY 與 OOP 或 FP 有沒有直接關係,大概就是 FP 的強制性,令你必須寫簡短流程的函式或方法,通常這會讓重複流程較容易觀察出來,接下來就看開發者要不要重構了。
結論就是,不用特別為了宣揚 FP 而去揚棄 OOP,不用去比較兩者,就單純地去認識 FP 就好,到某些程度之後,可以試著去學些純 FP 的語言,因為從 JavaScript 來認識 FP,還是有許多 FP 特性你不會接觸到。
若哪天 FP 較有心得且能掌握了,進一步地,需要 FP 就 FP,需要 OOP 就 OOP,必須結合 OOP 與 FP 時就去做,現代一些融合 FP、OOP 多重典範的框架,多半也就能掌握了。
可以學習到不同的程式設計,謝謝囉~
仔細看得過程有發現錯字~喔~
OOP 的 fumction 不是 Pure 的
已更新 感謝~ 看得很仔細
看完對FP很有興趣很認真看
這精美的排版,原來去年的系列就有追蹤了
今年也請加油!
謝謝~~ 是真的每天都想棄賽的概念
但沒想到寫文章還有大大可以幫忙糾正覺得很划算
寫這些文章最大的收穫莫過於大大們給我們建議了,我目前在寫 kotlin 版本的 functionl programming,一起加油吧!
function calculationBMI(bmi
if(bmi < 0) {
return '資料錯誤'
} else if(bmi < 18.5) {
return '過輕'
} else if(bmi >= 18.5 && bmi < 24) {
return '正常'
} else if(bmi >= 24 && bmi < 27) {
return '過重'
} else if(bmi >= 27 && bmi < 30) {
return '輕度肥胖'
} else if(bmi >= 30 && bmi < 35) {
return '中度肥胖'
} else{
return '重度肥胖'
}
}
calculationBMI(20); // '正常'
寫出這個範例的人可能需要多瞭解一下 else if
跟 return
的功能喔XD
稍微重構一下就能夠變成下面這樣了
function calculationBMI(bmi) {
if (bmi < 0) return '資料錯誤';
if (bmi < 18.5) return '過輕';
if (bmi < 24) return '正常';
if (bmi < 27) return '過重';
if (bmi < 30) return '輕度肥胖';
if (bmi < 35) return '中度肥胖';
return '重度肥胖';
}