本文章同時發佈於:
大家好,這次要來跟大家介紹FP的Maybe,我不會介紹到Monad等太複雜的FP元素,會以
遇到什麼問題,該怎麼解決來介紹
不然Maybe要用理論來介紹實在太抽象了XD。
在一個巢狀物件裡,要取得內層的值,需要一層一層檢查此
key
是否存在
比如說:在某個部落格網站,title
, subTitle
, description
都是可寫可不寫的。但iconURL
在網頁上一定要顯示,所以我們要一層一層拆開獲得iconURL
,如果iconURL
不存在就要有預設值。
一層一層拆開要注意,必須要拆一層就檢查key
是否存在,不然如果此key
不存在你還對他往下找key
就會爆炸。
比較傳統的做法是這樣:
function showIconURL (iconURL) {
console.log(iconURL)
}
const APIData = {
article: {
title: {
subTitle: {
description: {
iconURL: 'https://imgur.com/j1cOaSZ.jpg'
}
}
}
}
}
let showIconURL
if (
APIData.article &&
APIData.article.title &&
APIData.article.title.subTitle &&
APIData.article.title.subTitle.description
) iconURL = APIData.article.title.subTitle.description.iconURL
else iconURL = 'https://imgur.com/3NaQeyw.jpg'
showIconURL(iconURL)
這會產生兩個問題:
iconURL
來儲存值,再傳進showIconURL
。先說說第1個問題,多了這個變數其實不好,因為我們只是要「取得iconURL
並且用showIconURL
顯示」,多了一個變數就會讓我們多一份心力去注意她「什麼時候被改」。
這個範例很短你可能無法感受到,但當程式碼有幾百行的時候,你首先觀察到到let showIconURL
在第1
行,並且2~50
行是對他處理,但在第100
行你看到showIconURL(showIconURL)
時候你還是很難保證他是如你2~50
行所觀察到的值,因為51~99
行有可能變動。
所以我們可以改成這樣:
function showIconURL (iconURL) {
console.log(iconURL)
}
const APIData = {
article: {
title: {
subTitle: {
description: {
iconURL: 'https://imgur.com/j1cOaSZ.jpg'
}
}
}
}
}
const iconURL =
APIData.article &&
APIData.article.title &&
APIData.article.title.subTitle &&
APIData.article.title.subTitle.description &&
APIData.article.title.subTitle.description.iconURL ||
'https://imgur.com/3NaQeyw.jpg'
showIconURL(iconURL)
太好了,不用去在意這個變數的變化了。
事實上這就是FP的不變性,不變性的目的是讓值無法修改,這樣我們就不用擔心值被變的事情,因為變數不可變你就得想些方法避開再次賦值的動作,而當你避開後你甚至會發現
欸...其實這根本不用存變數
所以程式碼可以變成這樣:
function showIconURL (iconURL) {
console.log(iconURL)
}
const APIData = {
article: {
title: {
subTitle: {
description: {
iconURL: 'https://imgur.com/j1cOaSZ.jpg'
}
}
}
}
}
showIconURL(
APIData.article &&
APIData.article.title &&
APIData.article.title.subTitle &&
APIData.article.title.subTitle.description &&
APIData.article.title.subTitle.description.iconURL ||
'https://imgur.com/3NaQeyw.jpg'
)
程式裡面哪些變數真的得存,哪些根本不用會越來越明確,所以當程式因某變數出現問題你可以更清楚推斷哪邊「可能有變化」,而不是「Damn我全部的程式都要看一遍,因為全部都有可能會變」。
因為不能改變數,所以Maybe只能透過「將參數演算,並再傳入下個演算」的方式來取值,所以取值就從「改變數變成了演算」。
聽起來有點抽象,我們可以用第二個巢狀的解決方式來解釋。
Maybe的概念即是:
將一個可能存在或不存在的值包在此容器,我們必須打開容器才能取值。因為必須開容器,所以我們可以定義好如果沒值我們要怎麼處理
大家可以先看看範例比較有感覺,
const M = require('ramda-fantasy').Maybe
const Just = M.Just
const Nothing = M.Nothing
function showIconURL (iconURL) {
console.log(iconURL)
}
const APIData = {
article: {
title: {
subTitle: {
description: {
iconURL: 'https://imgur.com/j1cOaSZ.jpg'
}
}
}
}
}
const get = k => obj => k in obj ? Just(obj[k]) : Nothing()
showIconURL(
get('article')(APIData)
.chain(get('title'))
.chain(get('subTitle'))
.chain(get('description'))
.chain(get('iconURL'))
.getOrElse('https://imgur.com/3NaQeyw.jpg')
)
首先,我做了一個get
函數,他可以判斷是否此key
存在obj
,如果存在就回傳Just
容器,裡面包著obe[k]
這個值,如果不存在就回傳Nothing
容器,裡面就什麼值都沒有。
如果你跟一開始的我一樣有點盲,那可能是Nothing
的部分XD,即是:為什麼要回傳什麼都沒有的容器?
因為如果不存在你就不回傳容器,那我們就不能用定義好的「開容器時如果沒值要怎麼處理」來對應
在get('article')(APIData)
回傳了一個容器的時候,可再執行chain
函數,
以第1個chain
來說,會將容器打開並取值並丟入callback
,即:get('title')(容器取出來的值)
,之後又會吐出新的容器給第2個chain
的callback
使用,以此類推。
所以會有兩種情況,他們會有不同對應:
Just
容器被吐出來之後使用chain
函數:執行丟入chain
的callback
Noting
容器被吐出來之後使用chain
函數:一律不執行chain
,會一路跑到getOrElse
並執行它,而getOrElse
會把預設URL
回傳如果你又跟一開始的我一樣有點盲,那可能是chain
的部分XD,即是:開容器再把值丟入callback
,這什麼騷操作?!
其實你可能很早很早就在開容器取值了,他就是JS原生就有的map與flat
我們可以把array
想成一個容器,1
為容器內的值,我們只用map
再用另一個容器裝著並處出來,你會發現map
有開容器的效果,我們可以寫一個簡單的範例:
const get = item => [item] // get function規範一定也要return容器
[1].map(get)
// 獲得[[1]]
我們的確把1
取出並且放入新容器了,但我們回得到一個裝著容器的容器,並且裡面有1
,這不太對,所以我們要丟掉最外圍的容器,使用flat
可以達到此效果:
const get = item => [item] // get function規範一定也要return容器
[1]
.map(get)
.flat()
// 獲得[1]
而在ES10
有更簡化的版本flatMap
,可以把map
與flat
一次做完,
const get = item => [item] // get function規範一定也要return容器
[1].flatMap(get)
// 獲得[1]
而
chain
其實就是flatMap
你可能會問「啊幹嘛不叫flatMap
就好」,這只是社群討論導致XD。
所以我們再回頭回顧Maybe的範例
,他的邏輯就變成這樣
get('article')(APIData)
:丟出一個可能包含article
的容器A.chain(get('subTitle'))
:A容器利用chain
打開取值並丟入callback
,即:get('subTitle')(容器內的值)
,執行完畢後會因為容器包容器所以必須拆開一個容器再往下傳。.chain
都以此類推,但如果容器是Nothing
,即.chain
會一路不執行,直到跑到.getOrElse
執行後將URL
預設值回傳。我們可以再次回歸原本的問題來審視我們要解決什麼:
我們想要獲得一個資料在巢狀物件,並且我們想要避免key不存在的問題,所以當key不存在我們要有預設值
FP的Maybe
提供了一個方案,就是用容器規範你拿東西的行為
key
存在就回傳Just
容器並包著值key
不存在還是要回傳Nothing
容器,不然如果你不回傳我怎麼按照容器定好的行為來取值chain
,如果不存值就只能執行getOrElse
複習了這個思維後,可以再回頭看看範例,希望你更有感覺。
Optional chaining operator
可以用嗎?如果Maybe
看不太懂,但還是有遇到這個問題的話,可以用Optional chaining operator
來解決這個問題,我在我這篇文章有介紹,使用方式如下:
function showIconURL (iconURL) {
console.log(iconURL)
}
const APIData = {
article: {
title: {
subTitle: {
description: {
iconURL: 'https://imgur.com/j1cOaSZ.jpg'
}
}
}
}
}
showIconURL(APIData?.article?.title?.subTitle?.description?.iconURL ?? 'https://imgur.com/3NaQeyw.jpg')
是不是簡單很多XD?你可能想問那既然有這個幹嘛還學Maybe
,這是因為Maybe
功能更強大,我們前面都在討論null
或undefined
為Nothing
,那如果[]
, {}
, ""
, -1
呢?
Maybe
即是定義好了何者為空,如果剛剛的範例裡API的設計是description
不存在為{}
空物件,那我們就可以定義好他:
const M = require('ramda-fantasy').Maybe
const Just = M.Just
const Nothing = M.Nothing
function showIconURL (iconURL) {
console.log(iconURL)
}
const APIData = {
article: {
title: {
subTitle: {
description: {}
}
}
}
}
// 先定義好`{}`也是空
const get = k => obj => (k in obj && Object.keys(obj[k]).length !== 0) ? Just(obj[k]) : Nothing()
showIconURL(
get('article')(APIData)
.chain(get('title'))
.chain(get('subTitle'))
.chain(get('description'))
.chain(get('iconURL'))
.getOrElse('https://imgur.com/3NaQeyw.jpg')
)
這樣我們也會取得預設URL
。
(P.S.事實上我認為如果不存在就是null
,不應該再塞什麼[]
, {}
, ""
, -1
,但現實跟理想不同,比如說第三方API或舊專案就是把{}
當作空的意思,你也無法怎麼辦,這時候Maybe
就派上用場了)
大家都說JAVA是OOP
的典範,事實上在JDK 8
之後JAVA導入了許多FP
的特性,而其中一個名叫Optional
的API很好解決了JAVA的NullPointErexception
,就跟JS的null
或undefined
爆炸是一樣的問題。
事實上Optional
的實作概念就是Maybe
,大家可以去看看良葛格大大的Optional 與 Stream 的 flatMap,讓我受益良多。