iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Software Development

Functional Programming For Everyone系列 第 23

Day 23 - Either Monad

到目前為止,我們介紹 Maybe Monad 其是專門處理無值情境以及 IO Monad 則是處理同步計算的 effect,例如 console.log, localStorage 等等的,而這些操作基本上是不會失敗的。接下來今天來介紹專門處理錯誤情境的 Either Monad。

但在這之前先看看我們平常是如何處理錯誤情境的

Error Handle in JS

在 JS 的世界裡,常見處理錯誤的情境就是

  • if/else: 針對情境切出各式各樣分支,在各分支內進行相對的邏輯處理
  • throw/catch: 根據不同的情境,當發生錯誤時丟出其錯誤,通常會搭配 try...catch...

舉例來說,現在要驗證使用者輸入的資料,其結構是

{
  name: 'jing',
  phone: '0916888888',
  email: 'jing.open.to.opportunity@gmail.com'
}

而以下為驗證的函式

const isValidEmail = email => {
  const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(email);
}

const isValidPhone = phone => /^09\d{2}-?\d{3}-?\d{3}$/.test(phone)

const isValidName  = (name)  => name.trim() !== '';

首先,先來看看如果是用 if/else, 該如何處理

if/else

const runValidate = ({ name, email, phone }) => {
  if (!isValidName(name)) {
    return 'invalid name';
  } else if (!isValidPhone(phone)) {
    return 'invalid phone';
  } else if (!isValidEmail(email)) {
    return 'invalid email';
  } else {
    return { name, email, phone };
  }
};

const result = runValidate({
  name: '',
  phone: '0916888888',
  email: 'jing.open.to.opportunity@gmail.com'
}) // "invalid name"

if(typeof result === 'string'){
     console.error(result);
}

那再來看看 throw/catch 的版本

throw/catch

const runValidate = ({ name, email, phone }) => {
  if (!isValidName(name)) {
    throw new Error('invalid name');
  } else if (!isValidPhone(phone)) {
    throw new Error('invalid phone');
  } else if (!isValidEmail(email)) {
    throw new Error('invalid email');
  } else {
    return { name, email, phone };
  }
};

try {
  runValidate({
    name: '',
    phone: '0916888888',
    email: 'jing.open.to.opportunity@gmail.com',
  });
} catch (e) {
  console.error(e); // "invalid name"
}

可以看到無論是 if/else 或是 throw/catch 其處理都沒有辦法快速地知道這段程式到底在幹嘛。

或許有人覺得 throw/catch 不錯,但 throw/catch 並不是純函式,它不會 回傳(return) 錯誤而是 丟出(throw) 錯誤。還記得 pure function 那章提到的嗎,追求純函式其目的就是要達到 referential transparency,顯然 throw/catch 並不符合。

那廢話不多說開始介紹 Either Monad

Either Monad

Constructor

Either :: a -> Either c a

首先 Either 就跟 Maybe 一樣,皆由兩個 Type 所組合起來的,分別為 Either.Right 以及 Either.Left,可以把 Either.Right 想像成 happy path, 而 Either.Left 想像成 sad path.

image

// Either.Right :: a -> Either c a
const Right = (x) => ({
  x,
  inspect: () => `Right(${x})`,
});

// Either.Left :: c -> Either c a
const Left = (x) => ({
  x,
  inspect: () => `Left(${x})`,
});

Functor

Imgur

如上圖,Either Functor 就是將包覆在 Right 這個容器的值與 pure function 進行 compose,而運算的過程若有錯誤發生,就會切到 Left (sad path),在 Left 的 map method 不會有任何作用,只將錯誤傳遞下去。

implement

const Right = (x) => ({
  ...
  map: (f) => Right(f(x)),
  ...
});

const Left = (x) => ({
  ...
  map: (_) => Left(x),
  ...
});

example

Right(10)
    .map(R.add(1))
    .map(R.add(2))
    .inspect() // Right(13)
    
Left(10)
    .map(R.add(1))
    .map(R.add(2))
    .inspect() // Left(10)

在第一個例子由於都是走 happy path 其就會正常的 compose 下去,相較於第二個例子,由於一開始的值就已經是在 Left 內,無論有多少個轉換函式都 Left 都會無視。

之後可以看到 Right 其實就是 Identity,與 Left 這個錯誤處理的 types 結合就是 Either Monad

Chain

Imgur

由上圖可以看到,chain 就是將不同的鐵軌 (Right) 組合起來,而再組合的過程中也有可能會發生錯誤,所以又切分出其 sad path

implement

const Right = (x) => ({
  ...
  chain: (f) => f(x),
  ...
});

const Left = (x) => ({
  ...
  chain: (_) => Left(x),
  ...
});

example

const addOne = num => Right(R.add(1));
const error = () => Left(new Error('error'))

Right(10)
    .chain(addOne)
    .map(R.add(2))
    .inspect() // Right(13)
    
Right(10)
    .chain(error)
    .map(R.add(2))
    .inspect() // Left(Error: error)

可以看到第二個範例,中途如果突然出現了 error,軌道就會切到 Left 端,並且不會在對之後的函式進行 compose。

Applicative Functor

implement

const Right = (x) => ({
  ...
  isRight: true,
  ap: function (other) {
    return other.isLeft ? other : this.map(other.x);
  },
  ...
});

const Left = (x) => ({
  ...
  isLeft: true,
  ap: (_) => Left(x),
  ...
});

const lift2 = R.curry((g, f1, f2) => f2.ap(f1.map(g)))

example

lift2(R.add, Right(1), Right(1)); // Right(2)

lift2(R.add, Left('error'), Right(1)); // Left('error')

而 Applicative 的概念也是跟之前一樣,就不在贅述,只是這裡值得注意的是,我們需要新增 isLeft 以及 isRight,原因就是如果像之前一樣實作 fantasy-land 對於 apply 的定義,就會導致下面的程式噴錯,所以需要用 isLeft 以及 isRight 輔助實作 ap 時的邏輯。


const Right = (x) => ({
  ...
  ap: function (other) {
    return this.map(other.x);
  },
});

lift2(R.add, Left('error'), Right(1)); // 噴錯

fold & final code

而 Either 將值取出的方式就類似 pattern match,

implement

/**
 * Happy Path
 */

const Right = (x) => ({
  x,
  isRight: true,
  map: (f) => Right(f(x)),
  chain: (f) => f(x),
  ap: function (other) {
    return other.isLeft ? other : this.map(other.x);
  },
  fold: (_, g) => g(x),
  inspect: () => `Right(${x})`,
});

/**
 * Sad Path
 */

const Left = (x) => ({
  x,
  isLeft: true,
  map: (_) => Left(x),
  chain: (_) => Left(x),
  ap: (_) => Left(x),
  fold: (f, _) => f(x),
  inspect: () => `Left(${x})`,
});

const Either = {
  Left,
  Right,
  of: Right,
};

export default Either;

example

Right(10)
    .map(R.add(1))
    .map(R.add(2))
    .fold(console.error, console.log) // Right(13)

到這裡就簡單的介紹完最基本的 Either Monad,最後來改寫一下上面的範例

const isValidEmail = d => {
  const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(d.email) ? Right(d) : Left('invalid email');
}

const isValidPhone = d => /^09\d{2}-?\d{3}-?\d{3}$/.test(d.phone) ? Right(d) : Left('invalid phone')

const isValidName  = (d)  => d.name.trim() !== '' ? Right(d) : Left('invalid name');
isValidEmail({
      name: '',
      phone: '0916888888',
      email: 'jing.open.to.opportunity@gmail.com',
  })
  .chain(isValidPhone)
  .chain(isValidName)
  .fold(console.error, console.log); // invalid name

大功告成,但這樣似乎還有一個缺點,這樣變得我們的驗證函式會非常沒有彈性,因為每次都要放回全部的資料,如果今天格式改變了(ex: name -> username) 那這函式就沒有用了,之後有機會會提到如何處理這種情境,目前留給各位讀者思考一下,另外讀者們可以試著實作 point-free 版本的 Either,下一章討論!

小結

各位弟兄們又有新的小表出現拉,這裡統整目前提到的 Monad,感謝大家閱讀!

ADT Effect
Maybe 處理無值情境
IO 處理永不失敗的同步計算
Either 處理錯誤情境

NEXT: Task Monad

Reference

  1. Either & 圖片引用

上一篇
Day 22 - IO Monad
下一篇
Day 24 - Travserable
系列文
Functional Programming For Everyone30

尚未有邦友留言

立即登入留言