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: '[email protected]'
}

而以下为验证的函式

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: '[email protected]'
}) // "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: '[email protected]',
  });
} 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: '[email protected]',
  })
  .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 & 图片引用

<<:  [C 语言笔记--Day27] 6.S081 Lab syscall: Sysinfo ( I )

>>:  Day 24 - WooCommerce: 建立信用卡付款订单 (下)

使用python 模拟使用者输入 for Win

故事是这样的 ... 有个专案需要在执行过程中输入某些文字, 但不能使用按键精灵之类的软件去使用. ...

web C# 找出页面上的control

它可能在任何一个Control.要仔细找. Page.Controls -System.Web.UI...

Day 27:开始来学资料系结:使用目前所学,来个简单实作吧!(一)

资料系结的主题讲了好几天,那麽,就来小试身手一下,透过一个简单的实作,把这几天所学到的观念,试着运用...

Day029-透过Vuex-实作简易部落格-打包你的专案吧!

Vue:还记得第10天时,我们才刚认识Vue CLI,但你还记得我的介面吗? 接着,我们试试看这个指...

DAY12支持向量机演算法(续一)

昨天介绍完SMO算法第一步,今天就要来写这个方法第二步, 而第2步步骤:选取两个点,并计算上下界H和...