iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 2
14
Software Development

Functional Programming in JS系列 第 2

為什麼要學 Functional Programming?

What's Functional Programming

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) 就是

  • Style / Pattern 一種撰寫風格
  • Paradigm 一種規範
  • Mindset 抽象式思維

所以 FP 不是一種 “程式語言”,也與框架無關,以 JS 來說,不管你是用 Vue、React、Angular、原生 JS 都是可以用 FP 思維去撰寫的。

學習曲線

學 FP 很像是自己學烘焙

  • 一開始花很多錢買器具,也花了很長時間練習 (Pizza 麵團我可是做第三次才成功)
  • 漸漸上手後,覺得也太好玩了! 做了一堆烘焙點心然後狂推別人入烘焙坑
  • 別人開始覺得你很瘋狂,檸檬塔、各式麵包、瑪德蓮什麼都難不倒我,再也不吃隨便麵包店做的點心因為覺得難吃

雖然 FP 一開始的學習曲線真的頗高(要花許多時間理解),但是很快可以嚐到甜頭,尤其是在開發中大型專案時,不管維護或加功能都很容易。
https://ithelp.ithome.com.tw/upload/images/20200902/201064262Gcly6bGVZ.png

為何要學? 舊思維不好在哪

想學一個新東西之前,一定是因為舊有方式出現一些問題所以才想用新思維 / 撰寫風格 / 工具 / 框架 解決。

大家可能都有遇過,小案子寫起來很順沒有什麼太大問題,但當系統越變越複雜,就會發現難以維護、執行 DRY (Don't Repeat Yourself) 很困難、總是花很多時間 debug、不知道怎麼寫測試,甚至出現改 A 壞 B,改 B 又壞 C 的狀態。
https://blog.jerry-hong.com/static/terrible-world-ff336b116f3e43149be6532ba173e034.gif
(圖來自 J.H. Blog)

無法執行 DRY / 很難共用

因為一個函式包含非常多功能,所以很難共用也容易出現重覆。例如若需要寫有兩個函式, 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 時也會容易很多。

難以閱讀

一有判斷就是 ifwhileforswitch 用到底,判斷一多就會變成難以閱讀的窘境。接手別人專案往往要花很多時間理解別人在寫什麼

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 了呢?


參考文章

如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。


上一篇
開始用 javaScript 學 Functional Programming 囉之前言
下一篇
Buzz Word 1 : Declarative vs. Imperative
系列文
Functional Programming in JS30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
13
黃升煌 Mike
iT邦研究生 5 級 ‧ 2020-09-02 11:01:21

我也是 FP 愛好者,但覺得這篇文章可能有一點錯怪 OOP 了;個人覺得作者描述的問題比較偏向「程式設計」本身的問題,而非 OOP 的問題。

有心學習 OOP 的話 DRY、pure function、好讀好維護等等也都是完全辦得到的。

FP 和 OOP 的目標應該都是適度替程式設計附加抽象化,減少無謂的細節,更好閱讀及維護,溝通也會更加容易,只是 OOP 偏向更大觀點的抽象, FP 則偏向實作細節的抽象。

系統越來越龐大時,用 OOP 的觀點會很容易看出單元跟單元間的互動,但若只使用 FP 來看則會被密密麻麻的 function 互動給淹沒。

而進入實作層面時,用 FP 的觀念可以很快地把功能本身以抽象化的方式實作出來,OOP 雖然也做得到但確實就比繁瑣。

不得不說實作程式碼細節時用 FP 來寫真的很過癮 XD;但 OOP 也沒有作者提的那麼糟;以上是我自己的觀點,歡迎一起討論囉

看更多先前的回應...收起先前的回應...
hannahpun iT邦新手 4 級 ‧ 2020-09-02 11:20:15 檢舉

想請教一下您說得 "程式設計" 是什麼意思

就是程式語言本身使用的問題像是 iffor 這類的問題,使用 OOP 只要妥善設計也能大幅度消滅這些語法,但它實際還是存在只是包起來了;而使用 FP 其實也是把這些語法包裝起來使用,不搭配其他 library 一樣必免不了自己寫 if 的情境 (再怎樣包裝也要寫一次吧)。
樓下良葛格也是針對您的程式語言撰寫本身提出建議,可以參考看看囉

hannahpun iT邦新手 4 級 ‧ 2020-09-02 11:34:33 檢舉

謝謝你們花時間給我建議,已經更新文章說法了,感謝指教

系統越來越龐大時,用 OOP 的觀點會很容易看出單元跟單元間的互動,但若只使用 FP 來看則會被密密麻麻的 function 互動給淹沒。

大大的留言讓我茅塞頓開,曾經我也想在Server中大量使用FP,但後來就被這些pure function給淹掛了XD,如果偏向以實作面中來使用FP來抽象,那的確是很順手的思維,謝謝指教~

27
良葛格
iT邦新手 2 級 ‧ 2020-09-02 11:17:43

不建議從 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 多重典範的框架,多半也就能掌握了。

hannahpun iT邦新手 4 級 ‧ 2020-09-02 11:36:16 檢舉

感謝願意花時間給我建議,已經更新文章說法了,又學到一課
謝謝~~

良葛格/images/emoticon/emoticon07.gif/images/emoticon/emoticon32.gif
第二天就有台灣Java界的大人物來給建議!
看來值得追蹤!

w2sw2sw2s iT邦新手 5 級 ‧ 2020-09-12 09:32:20 檢舉

FP 是蠻不錯的思考方式,良葛格的補充也很清楚
感謝作者的好文!

0
小碼農米爾
iT邦高手 1 級 ‧ 2020-09-02 11:19:07

貓咪動畫太傳神了,尤其是最後那幕,心已死。 ╰( ̄▽ ̄)╭

hannahpun iT邦新手 4 級 ‧ 2020-09-02 11:47:33 檢舉

真的

0
qpalzm
iT邦新手 1 級 ‧ 2020-09-02 11:41:50

可以學習到不同的程式設計,謝謝囉~
仔細看得過程有發現錯字~喔~
OOP 的 fumction 不是 Pure 的/images/emoticon/emoticon12.gif

hannahpun iT邦新手 4 級 ‧ 2020-09-02 11:47:01 檢舉

已更新 感謝~ 看得很仔細 /images/emoticon/emoticon39.gif

qpalzm iT邦新手 1 級 ‧ 2020-09-02 14:52:52 檢舉

看完對FP很有興趣很認真看/images/emoticon/emoticon39.gif

0

這精美的排版,原來去年的系列就有追蹤了
今年也請加油!

hannahpun iT邦新手 4 級 ‧ 2020-09-05 10:37:02 檢舉

謝謝~~ 是真的每天都想棄賽的概念
但沒想到寫文章還有大大可以幫忙糾正覺得很划算

0
hungyanbin
iT邦新手 5 級 ‧ 2020-09-17 14:59:34

寫這些文章最大的收穫莫過於大大們給我們建議了,我目前在寫 kotlin 版本的 functionl programming,一起加油吧!

0
ytyubox
iT邦新手 5 級 ‧ 2020-09-20 22:27:47

我不懂 Javascript 也不懂 Functional programming,剛好看到這部影片,分享一下
Yes

hannahpun iT邦新手 4 級 ‧ 2020-09-20 23:53:46 檢舉

看完想學 Haskell /images/emoticon/emoticon07.gif
感謝分享

0
ltony1024
iT邦新手 5 級 ‧ 2021-08-04 11:56:26
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 ifreturn 的功能喔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 '重度肥胖';
}
0
lolmuta
iT邦新手 5 級 ‧ 2022-03-16 09:24:19

請教一下,

match... on ... otherwise 

是如何實作的呢?

我要留言

立即登入留言