首先JavaScript毕竟不是函数式语言,所以不必过分追求漂亮的函数式的实现。
用到的库:ramda,ramda-fantasy
const R= require('ramda');
const {Maybe,Either} = require('ramda-fantasy')
Either是用来处理错误的管道,Right中的值将被继续传递,Left中的值将收集,并打断后面的调用。Either被用来创建一个不被错误干扰的顺序执行的方法流。
例:验证一个文本输入框
const cNameReg = /^[\u4E00-\u9FA5]/;
const vEmpty = v => v.length !== 0 ? Either.Right(v) : Either.Left(new Error('必填!'))
const vMin = v => v.replace(/\./g,'').length >= 2 ? Either.Right(v) : Either.Left(new Error('长度需要多于2个字符! '))
const vMax = v => v.length <= 15 ? Either.Right(v) : Either.Left(new Error('长度需少于15个字符! '))
const vChinese = v => cNameReg.test(v.trim()) ? Either.Right(v) : Either.Left((new Error('不可包含英文、数字以及特殊字符')))
const getEitherName = R.composeK(vChinese,vMax,vMin,vEmpty,Either.Right);//方法流被创建
//到这一步Either的作用其实就结束了,下面都是帮助函数,不重要并且有点啰嗦。
let isValidated = ()=> state.nameErrMsg = null;
let isError = (err)=> state.nameErrMsg = err.message;
let EitherErrorOrValidated = Either.either(isError,isValidated);
EitherErrorOrValidated(eitherName(inputValue));
//可以写成更直观的形式
const eitherName = getEitherName(inputValue);
state.nameErrMsg = eitherName.isRight ? null : eitherName.value.message;
上面的写法依旧不够好,因为它的验证方法中有很多重复的样板代码:isValidate ? Either.Right(v) : Either.Left(new Error(errMsg! )
可以再写一个高阶函数来抽出样板代码,这个函数名不太好起,暂且叫做highValidate吧
const highValidate = (cond,errMsg)=> R.ifElse(cond,Either.Right,()=>Either.Left(new Error(errMsg)));
//改写验证方法
const vEmpty = highValidate((v)=>v.length !== 0,'必填');
const vMin = highValidate((v)=>v.replace(/\./g,'').length >= 2,'长度需要多于2个字符');
const vMax = highValidate((v)=>v.length <= 15,'长度需少于15个字符!');
const vChinese = highValidate((v)=>cNameReg.test(v.trim()),'不可包含英文、数字以及特殊字符');
const getEitherName = R.composeK(vChinese,vMax,vMin,Either.Right);//方法流被创建
const eitherName = getEitherName(inputValue);
state.nameErrMsg = eitherName.isRight ? null : eitherName.value.message;
依旧不怎么好看,重复的提到了参数v,有种说废话的感觉,继续改写成无参风格
const highValidate = (cond,errMsg)=> R.ifElse(cond,Either.Right,()=>Either.Left(new Error(errMsg)));
const vEmpty = highValidate(R.complement(R.propEq('length',0)),'必填');
const vMin = highValidate(R.pipe(R.replace(/\./g,''),R.prop('length'),R.gte(R.__,2)),'长度需要多于2个字符');
const vMax = highValidate(R.pipe(R.prop('length'),R.lte(R.__,15)),'长度需少于15个字符');
const vChinese = highValidate(R.pipe(R.trim,R.test(cNameReg)),'不可包含英文、数字以及特殊字符');
const getEitherName = R.composeK(vChinese,vMax,vMin,vEmpty,Either.Right);//方法流被创建
const eitherName = getEitherName(inputValue);
state.nameErrMsg = eitherName.isRight ? null : eitherName.value.message;
上面的写法几乎没有废话,不过其中highValidate被调用了4次,有点啰嗦,另外,其中的函数式写法虽然精简,但看起来像是古文一般难以理解。为了可读性,我们不能那么极致的追求代码精简,得思考一种折中方案,这有点妥协艺术的味道了。
//我们还是先把验证条件单独拿出来。
const vEmpty = (v)=>v.length !== 0;
const vMin = (v)=>v.replace(/\./g,'').length >= 2;
const vMax = (v)=>v.length <= 15;
const vChinese = (v)=>cNameReg.test(v.trim());
//现在我们需要一种方法可以执行四次高阶函数highValidate,然后返回包装后的方法供composeK的链式调用。--ap
const vFns = R.ap(highValidate,[vEmpty,vMin,vMax,vChinese]);
//但是我们又忘记传递errMsg参数了,它是和验证方法一一对应的
const errMsgs = ['必填','长度需要多于2个字符','长度需少于15个字符','不可包含英文、数字以及特殊字符'];
const vFns = R.ap([(fn)=>highValidate(fn,errMsgs.shift())], [vEmpty,vMin,vMax,vChinese]);
最后的代码实现是:
const vEmpty = (v)=>v.length !== 0;
const vMin = (v)=>v.replace(/\./g,'').length >= 2;
const vMax = (v)=>v.length <= 15;
const vChinese = (v)=>cNameReg.test(v.trim());
const errMsgs = ['必填','长度需要多于2个字符','长度需少于15个字符','不可包含英文、数字以及特殊字符'];
const highValidate = (cond,errMsg)=> R.ifElse(cond,Either.Right,()=>Either.Left(new Error(errMsg)));
const vFns = R.ap([(fn)=>highValidate(fn,errMsgs.shift())], [vEmpty,vMin,vMax,vChinese]);
const getEitherName = R.composeK(...vFns.reverse());//方法流被创建
const eitherName = getEitherName(inputValue);
state.nameErrMsg = eitherName.isRight ? null : eitherName.value.message;
但是,上面的代码就没有问题吗?假使你在业务代码中看到highValidate和vFns两个方法,他们依然是和业务无关的干扰代码,
我们想要的是这样一种实现:我们给出验证逻辑和对应的错误提示,然后得到一个either对象,也就是说,highValidate和vFns
不该出现在代码中,它们应该被抽象到工具函数库。甚至说,getEitherName都不该出现。
我们试着把和业务无关的代码抽象成一个方法。
//仅仅是把前面的几个方法集中了而已,纯函数的好处在这里显现,没有依赖,所以可以随意组合
const validate = (Fns,errMsgs)=>{
const highValidate = (cond,errMsg)=> R.ifElse(cond,Either.Right,()=>Either.Left(new Error(errMsg)));
const vFns = R.ap([(fn)=>highValidate(fn,errMsgs.shift())],Fns);
return R.composeK(...vFns.reverse());
}
//为了调用灵活点,柯里化一下
const validate = R.curry((Fns,errMsgs)=>{
const highValidate = (cond,errMsg)=> R.ifElse(cond,Either.Right,()=>Either.Left(new Error(errMsg)));
const vFns = R.ap([(fn)=>highValidate(fn,errMsgs.shift())],Fns);
return R.composeK(...vFns.reverse());
});
const getEitherName = validate([vEmpty,vMin,vMax,vChinese],
['必填','长度需要多于2个字符','长度需少于15个字符','不可包含英文、数字以及特殊字符']);
const eitherName = getEitherName(inputValue);
把validate方法移到自建的ramda_utils工具库并在当前代码引入,最终全部代码如下
const R= require('ramda');
const {Maybe,Either} = require('ramda-fantasy')
const {validate} = require('../util/ramda_utils')
const cNameReg = /^[\u4E00-\u9FA5]/;
const vEmpty = (v)=>v.length !== 0;
const vMin = (v)=>v.replace(/\./g,'').length >= 2;
const vMax = (v)=>v.length <= 15;
const vChinese = (v)=>cNameReg.test(v.trim());
const errMsgs = ['必填','长度需要多于2个字符','长度需少于15个字符','不可包含英文、数字以及特殊字符'];
const getEitherName = validate([vEmpty,vMin,vMax,vChinese],errMsgs);
const eitherName = getEitherName(inputValue);
state.nameErrMsg = eitherName.isRight ? null : eitherName.value.message;
最终,我们得到只根业务相关的代码。(以上代码都经过测试,直接可用。)下一节想讲一下Maybe。