“纯”错误处理
说出来可能会让你震惊,throw/catch
并不十分“纯”。当一个错误抛出的时候,我们没有收到返回值,反而是得到了一个警告!抛错的函数吐出一大堆的 0 和 1 作为盾和矛来攻击我们,简直就像是在反击输入值的入侵而进行的一场电子大作战。有了 Either
这个新朋友,我们就能以一种比向输入值宣战好得多的方式来处理错误,那就是返回一条非常礼貌的消息作为回应。我们来看一下:
var Left = function(x) {
this.__value = x;
}
Left.of = function(x) {
return new Left(x);
}
Left.prototype.map = function(f) {
return this;
}
var Right = function(x) {
this.__value = x;
}
Right.of = function(x) {
return new Right(x);
}
Right.prototype.map = function(f) {
return Right.of(f(this.__value));
}
Left
和 Right
是我们称之为 Either
的抽象类型的两个子类。我略去了创建 Either
父类的繁文缛节,因为我们不会用到它的,但你了解一下也没坏处。注意看,这里除了有两个类型,没别的新鲜东西。来看看它们是怎么运行的:
Right.of("rain").map(function(str){ return "b"+str; });
// Right("brain")
Left.of("rain").map(function(str){ return "b"+str; });
// Left("rain")
Right.of({host: 'localhost', port: 80}).map(_.prop('host'));
// Right('localhost')
Left.of("rolls eyes...").map(_.prop("host"));
// Left('rolls eyes...')
Left
就像是青春期少年那样无视我们要 map
它的请求。Right
的作用就像是一个 Container
(也就是 Identity)。这里强大的地方在于,Left
有能力在它内部嵌入一个错误消息。
假设有一个可能会失败的函数,就拿根据生日计算年龄来说好了。的确,我们可以用 Maybe(null)
来表示失败并把程序引向另一个分支,但是这并没有告诉我们太多信息。很有可能我们想知道失败的原因是什么。用 Either
写一个这样的程序看看:
var moment = require('moment');
// getAge :: Date -> User -> Either(String, Number)
var getAge = curry(function(now, user) {
var birthdate = moment(user.birthdate, 'YYYY-MM-DD');
if(!birthdate.isValid()) return Left.of("Birth date could not be parsed");
return Right.of(now.diff(birthdate, 'years'));
});
getAge(moment(), {birthdate: '2005-12-12'});
// Right(9)
getAge(moment(), {birthdate: 'balloons!'});
// Left("Birth date could not be parsed")
这么一来,就像 Maybe(null)
,当返回一个 Left
的时候就直接让程序短路。跟 Maybe(null)
不同的是,现在我们对程序为何脱离原先轨道至少有了一点头绪。有一件事要注意,这里返回的是 Either(String, Number)
,意味着我们这个 Either
左边的值是 String
,右边(译者注:也就是正确的值)的值是 Number
。这个类型签名不是很正式,因为我们并没有定义一个真正的 Either
父类;但我们还是从这个类型那里了解到不少东西。它告诉我们,我们得到的要么是一条错误消息,要么就是正确的年龄值。
// fortune :: Number -> String
var fortune = compose(concat("If you survive, you will be "), add(1));
// zoltar :: User -> Either(String, _)
var zoltar = compose(map(console.log), map(fortune), getAge(moment()));
zoltar({birthdate: '2005-12-12'});
// "If you survive, you will be 10"
// Right(undefined)
zoltar({birthdate: 'balloons!'});
// Left("Birth date could not be parsed")
如果 birthdate
合法,这个程序就会把它神秘的命运打印在屏幕上让我们见证;如果不合法,我们就会收到一个有着清清楚楚的错误消息的 Left
,尽管这个消息是稳稳当当地待在它的容器里的。这种行为就像,虽然我们在抛错,但是是以一种平静温和的方式抛错,而不是像一个小孩子那样,有什么不对劲就闹脾气大喊大叫。
在这个例子中,我们根据 birthdate
的合法性来控制代码的逻辑分支,同时又让代码进行从右到左的直线运动,而不用爬过各种条件语句的大括号。通常,我们不会把 console.log
放到 zoltar
函数里,而是在调用 zoltar
的时候才 map
它,不过本例中,让你看看 Right
分支如何与 Left
不同也是很有帮助的。我们在 Right
分支的类型签名中使用 _
表示一个应该忽略的值(在有些浏览器中,你必须要 console.log.bind(console)
才能把 console.log
当作一等公民使用)。
我想借此机会指出一件你可能没注意到的事:这个例子中,尽管 fortune
使用了 Either
,它对每一个 functor 到底要干什么却是毫不知情的。前面例子中的 finishTransaction
也是一样。通俗点来讲,一个函数在调用的时候,如果被 map
包裹了,那么它就会从一个非 functor 函数转换为一个 functor 函数。我们把这个过程叫做 lift。一般情况下,普通函数更适合操作普通的数据类型而不是容器类型,在必要的时候再通过 lift 变为合适的容器去操作容器类型。这样做的好处是能得到更简单、重用性更高的函数,它们能够随需求而变,兼容任意 functor。
Either
并不仅仅只对合法性检查这种一般性的错误作用非凡,对一些更严重的、能够中断程序执行的错误比如文件丢失或者 socket 连接断开等,Either
同样效果显著。你可以试试把前面例子中的 Maybe
替换为 Either
,看怎么得到更好的反馈。
此刻我忍不住在想,我仅仅是把 Either
当作一个错误消息的容器介绍给你!这样的介绍有失偏颇,它的能耐远不止于此。比如,它表示了逻辑或(也就是 ||
)。再比如,它体现了范畴学里 coproduct 的概念,当然本书不会涉及这方面的知识,但值得你去深入了解,因为这个概念有很多特性值得利用。还比如,它是标准的 sum type(或者叫不交并集,disjoint union of sets),因为它含有的所有可能的值的总数就是它包含的那两种类型的总数(我知道这么说你听不懂,没关系,这里有一篇非常棒的文章讲述这个问题)。Either
能做的事情多着呢,但是作为一个 functor,我们就用它处理错误。
就像 Maybe
可以有个 maybe
一样,Either
也可以有一个 either
。两者的用法类似,但 either
接受两个函数(而不是一个)和一个静态值为参数。这两个函数的返回值类型一致:
// either :: (a -> c) -> (b -> c) -> Either a b -> c
var either = curry(function(f, g, e) {
switch(e.constructor) {
case Left: return f(e.__value);
case Right: return g(e.__value);
}
});
// zoltar :: User -> _
var zoltar = compose(console.log, either(id, fortune), getAge(moment()));
zoltar({birthdate: '2005-12-12'});
// "If you survive, you will be 10"
// undefined
zoltar({birthdate: 'balloons!'});
// "Birth date could not be parsed"
// undefined
终于用了一回那个神秘的 id
函数!其实它就是简单地复制了 Left
里的错误消息,然后把这个值传给 console.log
而已。通过强制在 getAge
内部进行错误处理,我们的算命程序更加健壮了。结果就是,要么告诉用户一个残酷的事实并像算命师那样跟他击掌,要么就继续运行程序。好了,现在我们已经准备好去学习一个完全不同类型的 functor 了。