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

熊掌联运广告联盟 最新域名优惠/VPS优惠/免备案CDN活动

本文章同时发布于:

  • Medium
  • 一与博客

大家好,这次要来跟他家介绍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,让我受益良多。

参考资料

  • 使用 Optional 取代 null
  • Optional 与 Stream 的 flatMap
  • ramda-fantasy/Maybe.md at master · ramda/ramda-fantasy

推荐:
[鼠年全马铁人挑战] Week20 - 超新手学前端 - JavaScript:监听

嗨 大家早安午安晚安 这周开始 JavaScript 的直播班了 发现自己吸收程度真的很慢很多都看不懂QQ 还好有提供直播录影链接 可以把不懂的重复看到懂~ 这周就来纪录学到的监听笔记吧:) 何谓监听…

树莓派挂载硬盘的奇怪问题

asmrziyuan大佬: 如题,有5.4TB纪录片,树莓派用jellyfin搭建的 硬盘是硬盘盒组装的RAID 每次树莓派重启后,有时候是/dev/sda有时候是/dev/sdb 如何实现自动识别并…

win10的powershell是本来就卡顿还是因为有病毒?

大帅锅大佬: powershell开机后过段时间就常驻内存,占用CPU100,重启关机反复这样,看网上帖子有说是win10的问题,也有说是中毒了,你们有没有碰到过这种情况? PS:按照win10bug…