本文章同时发布于:
MediumiT邦帮忙大家好,这次要来跟大家介绍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 showIconURLif ( APIData.article && APIData.article.title && APIData.article.title.subTitle && APIData.article.title.subTitle.description) iconURL = APIData.article.title.subTitle.description.iconURLelse iconURL = 'https://imgur.com/3NaQeyw.jpg'showIconURL(iconURL)
这会产生两个问题:
我们多了一个变数iconURL
来储存值,再传进showIconURL
。一层一层的拆开要重複打物件路径满恼人的。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').Maybeconst Just = M.Justconst Nothing = M.Nothingfunction 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个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').Maybeconst Just = M.Justconst Nothing = M.Nothingfunction 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,让我受益良多。