點燈坊

失くすものさえない今が強くなるチャンスよ

當 Maybe 遇見 Promise

Sam Xiao's Avatar 2021-04-24

當有 Function 回傳 Maybe,而後續的 Function 都在 Maybe 內執行時,卻又有 Function 回傳 Promise,這就造成 Maybe 內有 Promise 的窘境,實務上該如何處理這種兩層 Monad 呢 ?

Version

Vue 3.0.11
Sanctuary 3.1.0

Promise Maybe

maybe000

當輸入的書名找不到時,會顯示 N/A

maybe001

當找到書名時,就會繼續呼叫 API 查詢價錢。

maybe002

當 API 回傳 error response 時,則顯示 error message。

<template lang='pug'>
input(v-model='title')
button(@click='onClick') Submit
span {{ price }}
</template>

<script setup>
import { ref } from 'vue'
import { read, write } from 'vue3-fp'
import { pipe, propEq, map, andThen as then, otherwise, path } from 'ramda'
import { create, env, prop, flip, find } from 'sanctuary'
import { resolve } from 'wink-fp'
import getPrice from '/src/api/getPrice'

let { fromMaybe } = create({ checkTypes: false, env })

let data = [
  { id: 1, title: 'FP in JavaScript' },
  { id: 2, title: 'Elm in Action' },
  { id: 3, title: 'Speaking JavaScript' }
]

let title = ref('')
let price = ref('')

let onClick = pipe(
  read(title),
  propEq('title'),
  flip(find)(data),
  map(prop('id')),
  map(getPrice),
  map(then(path(['data', 'price']))),
  fromMaybe(resolve('N/A')),
  then(write(price)),
  otherwise(path(['response', 'data', 'error'])),
  then(write(price))
)
</script>

17 行

let data = [
  { id: 1, title: 'FP in JavaScript' },
  { id: 2, title: 'Elm in Action' },
  { id: 3, title: 'Speaking JavaScript' }
]
  • 使用 find() 查詢 title ,無論是否存在都會回傳 Maybe

  • 後續的處理也都會在 Maybe 內,包含呼叫 API 也會在 Maybe 內

  • API 會回傳 Promise,這就造成 Promise 在 Maybe 內,也就是 Promise Maybe

該如何處理 Promise Maybe 這種兩層 Monad 呢 ?

26 行

let onClick = pipe(
  read(title),
  propEq('title'),
  flip(find)(data),
  map(prop('id')),
  map(getPrice),
  map(then(path(['data', 'price']))),
  fromMaybe(resolve('N/A')),
  then(write(price)),
  otherwise(path(['response', 'data', 'error'])),
  then(write(price))
)

使用 pipe() 組合 onClick()

  • read(title):讀取 title state
  • propEq('title'):組合 find() 所需要的 callback
  • flip(find)(data):與 Array.prototype.find() 的差異是無論找不找得到都會傳 Maybe,因此不用擔心 undefined 問題
  • map(prop('id'))::在 Maybe 內讀取出 id property
  • map(getPrice):呼叫 getPrice() 從 API 查詢 price,會回傳 Promise
  • map(then(path(['data', 'price']))):關鍵在此行,Promise 在 Maybe 內,因此只能 map() 先拆第一層 Maybe,再用 then() 拆第二層 Promise,所有的 function 都在 map(then)
  • fromMaybe(resolve('N/A')):處理 Nothing Maybe,將 N/A 包成 Promise 回傳
  • then(write(price)):處理 Resolved Promise,最後寫入 price state
  • otherwise(path(['response', 'data', 'error'])):從 Rejected Promise 內取出 e.response.data.error 並回傳 Promise
  • then(write(price)):將 error 寫入 price state

Conclusion

  • Promise Maybe 乍聽很嚇人,其實只要把握用 map()chain() 處理 Maybe,用 then() 處理 Promise,然後將所有 function 處理都寫在 map(then()) 內即可

Reference

Sanctuary, find()
Sanctuary, Maybe