函数式编程:对if-else的另一种解释
Posted treeShaking
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式编程:对if-else的另一种解释相关的知识,希望对你有一定的参考价值。
本来想给本文起个名字叫‘避免代码中的if-else’,或‘重构if-else’,但实际上都不达意。代码中并不能彻底避免if-else,比如,if-else的分支只代表业务逻辑的不同分支,那么写成如下是合理的:
if (condition) {
do task1
} else {
do task2
}
// 或
condition ? do task1 : do task2
但是如果业务逻辑分支变得多而校验也随之变得复杂时,你可能会看到下面可读性很差的代码:
const validateBranching = ({ name, type, email, phone }) => {
if (!isValidName(name)) {
return Required('name');
} else if (type === 'email') {
} else if (phone && !isValidPhone(phone)) {
return Optional(InvalidPhone(phone));
} else {
return { type, name, email, phone };
}
} else if (type === 'phone') {
if (!isValidPhone(phone)) {
return InvalidPhone(phone);
} else {
return { type, name, email, phone };
}
} else {
return InvalidType(type);
}
};
以上是对输入的名字,邮箱,手机号进行的校验。泄这种代码实属正常,这是最直接的逻辑,运行效率是最高的。只不过她不优雅,甚至会让你陷入麻烦。从直观上看,一下子找不出业务的正向逻辑,无法分清,哪些是业务分支,哪些是出错处理,哪些是某个业务的嵌套分支,在调试时尤其让你烦恼的一遍遍的梳理这一团乱麻的if-else,有人可能会对这样的代码做些改变:
}
};
const assertPhone = (phone, wrapper=id) => {
if (!isValidPhone(phone)) {
}
};
const validateThrow = ({ name, type, email, phone }) => {
if (!isValidName(name)) {
throw Required('name');
}
switch (type) {
case 'email':
if (phone) assertPhone(phone, Optional);
return { type, name, email, phone };
case 'phone':
assertPhone(phone);
return { type, name, email, phone };
default:
throw InvalidType(type);
}
};
以上代码最突出的优化,应该是对部分逻辑进行函数的封装,并给函数起了个通俗的名字,如 assertPhone
,一看就知道是在做出错处理。这使得,你的出错处理开始与业务逻辑在阅读体验上分离开来了。但是这么做,必须在出错处理函数里抛出异常。据说抛出异常并不很好:
如果你在浏览器里运行,就不应该在业务逻辑里捕获异常,因为抛出异常,就需要捕获异常,如果只单处理了业务逻辑错误,而忽略了浏览器环境可能出现的错误,你的程序就不好调试了。
你后续得写那些try-catch,多余的东西总会让我难受。
那么有没有比上面更好的办法。当然,只要使用monad思想写一套出错处理的函数式编程库就行。而且现在已经有了。本文使用了folktale的Data.Either,folktale是个大型的函数式编程库,我们现在只用Data.Either,顺便提一句,官方出了个换了名字的另一个库result,区别是result在函数命名上更倾向于做出错处理,因为result的正向逻辑是Ok,反向是Error,而Data.Either的正向是Right,反向是Left,我觉得都没问题,但是我们希望不只是做出错处理,Left也有可能是逻辑的一部分。所以以Data.Either为例。先解释一下这个库的用法。
安装:
$ npm install data.either
通常用于取值的函数有:
import { Right, Left, of, Either } from 'data.either'
Right(1).get() // 1, Right构造一个对象向,约定存储正向逻辑的值
of(1).get() // 1, of等价于Right
Left(1).get() // throw new TypeError("Can't extract the value of a Left(a).")
Left(1).getOrElse(2) // 2, get用于获取存于Right对象内的值,如果用于Left会报错,但Left可以使用getOrElse获取一个默认值
Right(1).getOrElse(2) // 1, 由于Right可以通过get合法的取到值,所以不会反悔默认值
Left(1).merge() // 1, 取出对象中的值,无论存于Left和Right对象中
Right(1).merge() // 1, 取出对象中的值,无论存于Left和Right对象中
然后来对Left和Right的其他常用成员函数做一个分类
Left和Right具有相同的成员函数,但是执行不同,区别在于,部分函数在Left对象上传播,部分函数在Right对象上传播,部分在两者上都起作用。所谓传播propagation,可以理解为忽略,由于函数式编程通常使用 .
进行连续执行逻辑,像一个链条,链条上每个环节就是逻辑处理,处理结果就在这个链条上流传,当然可以选择性忽略某个环节,忽略在形式上就像传播过去了一样,并且传播时携带了原有的上个环节的信息,常用的成员函数,在不同的对象上起作用,可以分类为如下
Right:
map,chain
Left:
leftMap,orElse
Right,Left:
bimap,fold,merge,getOrElse
其中最长用的应该是上面的前两者,而且 map,chain
和 leftMap,orElse
是对应的。这些函数的用法和名字都和函数式编程前面几节里的一样,属于约定俗成的。所以具体功能不再赘述,主要看如何使用。我们对本文开头出的业务重构一下:
import { Right, Left, of, Either } from 'data.either'
interface Props {
name: string,
type: string,
email: string,
phone: number
}
type CheckInfo = Either<string|Props, string|Props>
type FuncType = (v: Props) => CheckInfo
const testName: FuncType = v => {
return v.name.trim() === '' ? Left('no name value') : Right(v)
}
const testType: FuncType = v => {
switch (v.type) {
default: return Left('error type value')
}
}
return v.email.indexOf('@') === -1 ? Left('error email value') : Right(v)
}
const testPhone: FuncType = v => {
return isNaN(v.phone) ? Left('error phone value') : Right(v)
}
const maybeTestPhone: FuncType = v => {
return v.phone ? testPhone(v).leftMap(_ => 'email ok, but phone error') : Right(v)
}
}
const doIt = ({name, type, email, phone}: Props) => {
return of({name, type, email, phone})
.chain(testName)
.chain(testType)
}
首先对各个小模块进行封装,其实就算不使用either库,你也应该这么做,可以形成良好的可读性。从例子可知,你需要对最基本的校验模块进行封装,需要用的时候,就用chain进行组合。
如果你想看到更具普遍性的写法,稍加改造即可。
return v.type && v.type === 'email' ? Right(v) : Left(v)
}
const typeIsPhone: FuncType = v => {
return v.type && v.type === 'phone' ? Right(v) : Left(v)
}
const doIt2 = ({name, type, email, phone}: Props) => {
return of({name, type, email, phone})
.chain(testName)
.chain(maybeTestPhone)
.orElse(typeIsPhone)
.chain(testPhone)
.orElse(v => typeof v === "string" ? Left(v) : Left('error type value'))
以上写法可以看出模块组合和错误传播的能力,在直观上,你仅仅在写正向逻辑,每一个模块都有可能产生错误,但无需在中间捕获错误,因为错误会传播到最后。你只需关心结果是正确还是出错。这样的代码首先是清晰的,命令式的,然后还具有可重用,可重构的,同时还是 pointFree
的,也就是中间无实参传递。
运行一下
const result = doIt({
name: 'yangdechao',
type: 'email',
email: 'skji@163.com',
phone: 123123123,
}).merge()
/*
{ name: 'yangdechao',
type: 'email',
email: 'skji@163.com',
phone: 123123123 }
*/
const result2 = doIt2({
name: 'yangdechao',
type: 'phone',
email: 'skji163.com',
phone: 123123123,
}).merge() // phone ok, but email error
以上是关于函数式编程:对if-else的另一种解释的主要内容,如果未能解决你的问题,请参考以下文章