這個議程會使用 TypeScript 介紹 functional programming 中的常見概念與在 web front-end 中的應用。選用 TypeScript 是因為它的 type system 雖然不夠好,但已經足夠讓我們以 type 為出發點思考問題。同時它也有 JavaScript 的靈活性。
在像 Haskell 或是 OCaml 這種 ML 系的 FP 語言中,常會看到由 sum type 和 product type 組合而成的 algebraic data type ,例如:
在 JS 裡我們沒有 sum type 和 product type ,但我們可以模擬一下,並用 TypeScript 描述它們:
此時我們用 TypeScript 的 discriminated union 來替代 sum type ,當用到這種資料時,可以把 switch
當成陽春版的 pattern matching 用。同時以 object 替代 product type 。
有了陽春版的 sum type 和 product type ,現在我們可以開始建立抽象層。
如果大家有看過 SICP 影片的話,可以注意到在構造有理數時,課程慢慢變得有趣了起來。
We're going to solve that problem by using this incredbly powerful design strategy that you already seen us use over and over.
And that's the strategy of wishful thinking.
— Harold Abelson
因為 JS 現在有了 module system ,我們也可以把同樣的技巧用在 JS 上。
ToDoItem
今天我們的目標是做一個 ToDo app ,從最底部開始思考,我們的 ToDo item 該長什麼樣子呢?
接下來我們要能創造、存取、更新這個 ToDo :
注意在這裡我手動 curry 了 update
函數,這樣做會讓在保持每個 ToDo 不可變的同時簡化我們的程式碼。
根據 SICP 的說法,只要 module 的介面沒變,我們完全可以用另外一種方式實現 ToDoItem
:
同時也會發現,我們能把 tuple 當成 product type 用。
ToDoList
做完 ToDoItem
,再用一樣的思路做 ToDoList
:
如果不喜歡一直傳遞最新的物件,我們也可以把這種風格的 ToDoList
包裝成一個 OOP class
:
但為什麼那麼堅持要手動 curry ,並把要修改的物件,放在最後一個參數呢?
因為這樣做的話,要是哪一天 ToDoList
被放到 Promise
中,我們還是可以這樣操作它:
要是我們把 ToDoList
放在 React state 裡面,也可以直接操作它:
這讓我本來很期待 |>
pipeline operator :
那時候還是所謂的 F# style pipeline operator (為什麼不叫 OCaml style 啊,兒子比老子有名,這樣對嗎?)。但現在的 Hack style pipeline operator 寫起來就比較囉唆了:
我不喜歡 Hack pipeline ,但是 proposal 裡提到:
Both Hack pipes and F# pipes respectively impose a small syntax tax on different expressions:
Hack pipes slightly tax only unary function calls, and
F# pipes slightly tax all expressions except unary function calls.
可見 JS 為了過去的舊設計,選擇了痛苦比較少的道路。
But with F# pipes,
value |> someFunction + 1
is still valid syntax – it’ll just fail late at runtime, becausesomeFunction + 1
isn’t callable
但 proposal 內才剛說完要考慮到未來會有 operator overloading ,就舉例子說 someFunction + 1
不會是個函數還滿怪的。在 FP 的世界,你當然可以讓兩個函數被 +
起來,而且結果是個新的函數。
反而後面提到 F# style pipeline 會帶來實作上的困難還比較有說服力。
大家可以自己讀一下 tc39/proposal-pipeline-operator ,看看你喜歡哪一種 pipeline ?
Redux action 大概長這樣:
這怎麼看都是個 sum type 吧?事實上因為 Redux 一開始是「借鑑」 Elm 的, action 長得像 ML 語言的 sum type 也是情理之中。既然是個 sum type ,我們沒道理只在 payload 裡面放 primitive types 。
我們做個裝著 action 的 action !
於是使用 redux-thunk
時,就能這樣合成我們的 thunk actions :
這樣在 root reducer 中,我們可以先處理外層 action ,再把裡面的 action 交給其他 reducers :
內層 reducer 現在只要更新收到的 todo 即可:
這樣相當於手動 compose reducers 。
當然現在不用這樣搞, Redux Toolkit 用一種比較「好懂」的方式處理這件事, createAsyncThunk
會自動產生不同狀態的 action types ,像是 some/action/pending
, some/action/fulfilled
, some/action/rejected
。
在介紹 CPS 變換之前,要先講一下什麼是 CPS ,也就是 continuation-passing style 。在 FP 的世界, continuation-passing style 指的是你的函數不回傳東西,而是把計算結果交給下一個函數當參數。
例如一個相加兩數的函數:
變成:
常寫 JS 的人大概已經聞到了熟悉的味道,是的, Node.js API 那些需要傳 callback 的函數就是 CPS 。
這邊大家可以注意,一般的程式能很簡單地轉成 CPS 程式,但 CPS 程式就沒法很簡單地轉成一般的程式。畢竟你不知道 callback 函數什麼時候會被呼叫。為此先有人引入了 deferred 物件,後來又誕生的 Promise/A+ 標準。
於是我們可以把 add
寫成:
在這裡我們不關心 add
是直接幫我們算出 a + b
,還是呼叫躲在 AWS 上的 API 幫我們算 a + b
,只要知道我們等一下會拿到一個 number
就好了。
但這個「等一下」仍然是個麻煩,因為不知道會等多久,我們沒法把 Promise<number>
直接拆成 number
。
在 MSFT 對 JS 的大力貢獻下, JS 世界有了 C# 那樣的 async/await
語法糖,現在我們可以這樣用:
從 add(1, 2).then(console.log)
到 const c = await add(1, 2)
就是一種 CPS 變換( CPS transformation )。
但 async/await
只能用在 Promise
上,卻不能用在其他類似的結構上。好在隨著 React 和 Vue 等前端 library/framework 決定他們「要搶著做程式語言該做的事」,我們現在有很多手段可以在不同的結構上做到 CPS 變換。
例如我們可以用 React Hooks 來 resolve Promise :
就能在 React 元件內這樣寫:
我們甚至可以用 React Hooks 拆解 RxJS 的 Observable :
但大概是因為 React Hooks 是個「半殘的抽象工具」,幾乎沒有人這樣實作 usePromise
和 useObservable
。倒是 Vue3 上有個 vue-compose-promise
提供了類似的介面:
到這裡,如果你有注意到我在做什麼的話,那當我用 React Hooks 對 array 做 CPS 變換時,你也不會驚訝了:
本來這邊想講 Observable
的,但因為太麻煩了所以講個 Promise
就好。
假設我們今天想弄個晚一點才會開始跑的 promise ,就叫它 LazyPromise
好了。我們一樣想用 then
把它們串起來,可以這樣做:
配上 OCaml style pipeline operator ,我們可以這樣用:
大概就這樣。