到目前为止,我们介绍 Maybe Monad 其是专门处理无值情境以及 IO Monad 则是处理同步计算的 effect,例如 console.log
, localStorage
等等的,而这些操作基本上是不会失败的。接下来今天来介绍专门处理错误情境的 Either Monad。
但在这之前先看看我们平常是如何处理错误情境的,
在 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
Constructor
Either :: a -> Either c a
首先 Either 就跟 Maybe 一样,皆由两个 Type 所组合起来的,分别为 Either.Right
以及 Either.Left
,可以把 Either.Right
想像成 happy path, 而 Either.Left
想像成 sad path.
// 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})`,
});
如上图,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 就是将不同的铁轨 (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。
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
<<: [C 语言笔记--Day27] 6.S081 Lab syscall: Sysinfo ( I )
>>: Day 24 - WooCommerce: 建立信用卡付款订单 (下)
故事是这样的 ... 有个专案需要在执行过程中输入某些文字, 但不能使用按键精灵之类的软件去使用. ...
它可能在任何一个Control.要仔细找. Page.Controls -System.Web.UI...
资料系结的主题讲了好几天,那麽,就来小试身手一下,透过一个简单的实作,把这几天所学到的观念,试着运用...
Vue:还记得第10天时,我们才刚认识Vue CLI,但你还记得我的介面吗? 接着,我们试试看这个指...
昨天介绍完SMO算法第一步,今天就要来写这个方法第二步, 而第2步步骤:选取两个点,并计算上下界H和...