Week20 - 用FP的Maybe来跟Null爆炸说再见吧! [高智能方程式系列]

本文章同时发布于:

MediumiT邦帮忙

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 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个chaincallback使用,以此类推。

所以会有两种情况,他们会有不同对应:

Just容器被吐出来之后使用chain函数:执行丢入chaincallbackNoting容器被吐出来之后使用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的範例,他的逻辑就变成这样

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功能更强大,我们前面都在讨论nullundefinedNothing,那如果[], {}, "", -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的nullundefined爆炸是一样的问题。

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

参考资料

使用 Optional 取代 nullOptional 与 Stream 的 flatMapramda-fantasy/Maybe.md at master · ramda/ramda-fantasy

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章