iT邦幫忙

2

Week20 - 用FP的Maybe來跟Null爆炸說再見吧! [高智能方程式系列]

本文章同時發佈於:


From Fantasy-Land-Specification

大家好,這次要來跟大家介紹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)

這會產生兩個問題:

  1. 我們多了一個變數iconURL來儲存值,再傳進showIconURL
  2. 一層一層的拆開要重複打物件路徑滿惱人的。

Maybe觀念之一 - 不變性

先說說第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

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容器,裡面就什麼值都沒有。

Just還稍能理解,但Nothing是什麼東西!?

如果你跟一開始的我一樣有點盲,那可能是Nothing的部分XD,即是:為什麼要回傳什麼都沒有的容器?

因為如果不存在你就不回傳容器,那我們就不能用定義好的「開容器時如果沒值要怎麼處理」來對應

get('article')(APIData)回傳了一個容器的時候,可再執行chain函數,

以第1個chain來說,會將容器打開並取值並丟入callback,即:get('title')(容器取出來的值),之後又會吐出新的容器給第2個chaincallback使用,以此類推。

所以會有兩種情況,他們會有不同對應:

  1. Just容器被吐出來之後使用chain函數:執行丟入chaincallback
  2. 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,可以把mapflat一次做完,

const get = item => [item] // get function規範一定也要return容器
[1].flatMap(get)
// 獲得[1]

chain其實就是flatMap

你可能會問「啊幹嘛不叫flatMap就好」,這只是社群討論導致XD。


所以我們再回頭回顧Maybe的範例,他的邏輯就變成這樣

  1. get('article')(APIData):丟出一個可能包含article的容器A
  2. .chain(get('subTitle')):A容器利用chain打開取值並丟入callback,即:get('subTitle')(容器內的值),執行完畢後會因為容器包容器所以必須拆開一個容器再往下傳。
  3. 以下每個.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功能更強大,我們前面都在討論nullundefinedNothing,那如果[], {}, "", -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的nullundefined爆炸是一樣的問題。

事實上Optional的實作概念就是Maybe,大家可以去看看良葛格大大的Optional 與 Stream 的 flatMap,讓我受益良多。

參考資料


1 則留言

0
harveychan
iT邦新手 5 級 ‧ 2020-07-07 10:37:05

太神啦

我要留言

立即登入留言