表达式
一个常见的表达式:sql,例如:1
SELECT * FROM student;
从student表中获取所有的数据。
1 | CREATE TABLE Persons |
创建一张表。
表达式(expression)表达的是要什么,而不是怎么去做。
一些简单的javascrip示例
1 | // 示例1 求和 |
表达式形式1
2
3
4
5
6
7
8// 示例1 求和
const nums = [1,2,3,4,5]
const total = sum(nums)
// 示例2 获取所有及格的学生
const allStudent = [{score: 59, name: "xiaowang"},{score: 60, name: "xiaohong"}];
const passStudent = filter(allStudent, ({score}) => score > 60)
//或者
const passStudent = filterAllPass(allStudent)
示例中并没有说sum和filter或者filterAllPass是哪里来的。这里只是想表达一下表达式的简洁。然而简单的封装一下1
2
3
4
5
6
7
8
9
10const filterAllPass = (allStudent) => {
const passStudent = []
for (let i = 0; i< allStudent.length; i++) {
const student = allStudent[i]
if (student.score > 60) {
passStudent.push(student)
}
}
return passStudent;
}
就可以把指令式的封装成了函数式的表达式,难道函数式是指令式的多此一举吗?
组合
函数式绝不是指令式的多此一举。如果真的需要filterAllPass函数。函数式编程也会有自己的一套解决方法组合.简单的说filterAllPass就是遍历(这里是filter不是map)、取属性、判断大小组合起来的。如果
- 遍历 -> filter
- 取属性 -> props
- 判断大小 -> gt
- 组合 -> .
那么 const filterAllPass = filter(compose(gt(60), props("score")))
遗憾的是javascript不可以自定义操作符,也没有.操作符来代替compose,所以javascript的代码看起来总是会比较长。
haskell版本:filterAllPass = filter $ (> 60) . (props "score")
兼容性
monad
实现了map函数的数据结构就是monad。例如Number.of(1).map(+2).map(*3) => Number.of(9),此时的Number就是一个monad。不严格的说。Promise就是monad。Promise提供了then函数返回新的Promise,就像Monad提供map函数返回新的Monad一样。
Maybe
Maybe是一个Monad,它能在有值(Just)的时候进行map没值(Nothing)的时候啥都不干。例如:Maybe.of(just 3).map(+3).map(*3)=>Just 18。Maybe.of(Nothing).map(+3).map(*3)=>Nothing。
一些优秀的js库
如果你有考虑过函数const filterAllPass = filter(compose(gt(60), props("score")))在接收异常的参数的时候会怎么样时,已经有一些优秀的库考虑了。
比如lodash的filter,在你调用的时候无论如何都会返回一个数组给你。即使你用undefined做为参数得到的也是数组。还有ramda的props函数,总是会返回你想取的属性的值,不管这个属性存在不存在。也不管你想操作的对象是不是undefined。这种特性很像Maybe的特性,有值就会处理,没有值就不去处理。反正不会因为边界情况而中断。
实际项目中的一些例子及lodash、ramda的简介
素材1
1 | function sumProductAmount2() { |
sumProductAmount2函数内使用了each做为函数处理数组数据而不是使用for循环来处理。但是在整个实现上,还是延用了指令编程的思想。指明了一个数据要先判断是否是delete的数据,再去与迭代amount求和
lodash版本
1 | function sumProductAmount2() { |
lodash版本使用chain将products打包,使用申明型语句表明了去掉__type属性等于delete的数据,再将数据根据属性amount印射,然后对所有的数据进行迭代reduce((acc, x) => myMath.add(acc, x.amount), 0)。最后取值value(),整体上已经很好了。这样的函数完全可以写成一个lamda表达式(如果不怕难看的话):1
2
3
4
5const sumProductAmount2 = () => products => chain(products)
.reject(["__type", "delete"])
.map("amount")
.reduce((acc, x) => myMath.add(acc, x), 0)
.value()
lodash版本有一些美中不足的是reduce函数虽然已经很fp风格了。但是在语义上并没有尽善尽美。如果有一个函数.sumWith(myMath.add)用以代替.reduce((acc, x) => myMath.add(acc, x), 0)的话。易读性就会变得非常的好。
ramda版本
1 | import { reduce, map, prop, reject, propEq, compose } from 'ramda' |
ramda库没有语法糖,没有魔法银弹。看看lodash版本的reject。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var users = [
{ 'user': 'barney', 'age': 36, 'active': false },
{ 'user': 'fred', 'age': 40, 'active': true }
];
_.reject(users, function(o) { return !o.active; });
// => objects for ['fred']
// The `_.matches` iteratee shorthand.
_.reject(users, { 'age': 40, 'active': true });
// => objects for ['barney']
// The `_.matchesProperty` iteratee shorthand.
_.reject(users, ['active', false]);
// => objects for ['fred']
// The `_.property` iteratee shorthand.
_.reject(users, 'active');
// => objects for ['barney']
这个怪异的签名意思是
- 可以传递一个函数,当返回值为true时reject此数据。
- 可以传递一个对象,会使用
_.matche函数调用些对象返回一个函数,之后情况参照示例1. - 可以传递一个元组(其实就是长度为2的数组),会使用
_.matchesProperty调用返回一个函数,之后参数示例1. - 传递一个字符串,应用
_.property
换言之:
_.reject(users, function(o) { return !o.active; });这种写法是最初始提供的。_.reject(users, { 'age': 40, 'active': true });写法是_.reject(users, _matches({ 'age': 40, 'active': true }));的语法糖。_.reject(users, ['active', false]);是_.reject(users, _.matchesProperty('active', false));的语法糖_.reject(users, 'active');是_.reject(users, _.property('active'));的语法糖
并且这种调用形式在lodash里面被广泛使用。熟悉这种语法糖会感觉非常的方便。而不熟悉的人会感觉代码怪异难懂。
ramda函数库并没有类型的语法糖。要想表达属性__type等于delete必须乖乖使用propEq("__type", "delete"),没有这种形式["__type", "delete"]
下面介绍一下ramda与lodash的不同。ramda更容易写出pointfree的代码(也许lodash/fp也提供了类似方法)。
在示例中 函数sumProductAmount2只是为了返回一个函数对数据源products做处理。
ramda版本的函数并没有提及这个数据源products,它只是表明了一个函数该做的事。1
2
3
4
5compose(
reduce(myMath.add,0),
map(prop("amount")),
reject(propEq("__type", "delete"))
)
使用组合的方式组合三个匿名函数。(注意顺序是从右到左从下到上)
reject(propEq("__type", "delete"))过滤掉了所有__type属性为delete数据map将数据属性amount提取出来。reduce将所有的数据通过myMath.add救和,初始值设置为0。
这三点在介绍lodash版本的时候说过了。这里再说一次的意图是ramda和lodash处理的思想是一致的。只是在写法上不同(lodash/fp也许也能这么写,所以ramda与lodash也许只有api设计的不同)
还有一个点reduce(myMath.add,0),由于我知道myMath.add中没有使用this并且知道reduce会为迭代函数提供两个参数acc,value。我才敢这样做。为了更安全可以写成reduce((acc,value) => myMath.add(acc,value),0)。这种提及了数据的写法很丑。但是让人感觉到安全,因为看到了数据的流向。所以在函数编程里面,尽量不要使用this。因为this本身就是为面向对象设计,this会让函数不纯。
看不到数据的流向会让人调试很麻烦。除了使用tdd开发方式外。还可以通过帮助函数进行调试。1
2
3
4
5
6
7
8
9
10const helper = data => {
console.log(data); //break here;
return data;
}
compose(
reduce(myMath.add,0),
helper,
map(prop("amount")),
reject(propEq("__type", "delete"))
)
helper不会影响到正常的流程,只是会提供了一个看见数据的机会。
之前我们提到过reduce(myMath.add,0)形式没有sumWith更语义化,在lodash版本里面我们没法改,但是ramda版本我们是可以替换的。
1 | const sumWith = fn => reduce(fn,0); |
don’t repeat yourself (dry)
1 | // myMath.add是一个避免浮点数加法精度问题的加法函数函数 |
上文定义了两个过滤器。代码几乎一模一样,一个统计authorization_type 为 1的 一个统计authorization_type 为 0的。
上述代码违背了dry,
原作者内心:我没有重复编码,第二段是我
CV的。
先用lodash重构一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18filter('countSMS2', function () {
return function (products) {
return chain(products)
.reject(["__type", "delete"])
.map(({ authorization_type: at, authorization_numeric: an }) => +at === 1 ? an : 0)
.reduce((acc, x) => myMath.add(acc, x), 0)
.value()
};
})
filter('countProject2', function () {
return function (products) {
return chain(products)
.reject(["__type", "delete"])
.map(({ authorization_type: at, authorization_numeric: an }) => +at === 0 ? an : 0)
.reduce((acc, x) => myMath.add(acc, x), 0)
.value()
};
})
接着把公共的部分提出去1
2
3
4
5
6
7const sumByType = type => products => chain(products)
.reject(["__type", "delete"])
.map(({ authorization_type: at, authorization_numeric: an }) => +at === type ? an : 0)
.reduce((acc, x) => myMath.add(acc, x), 0)
.value()
filter('countSMS2', () => sumByType(0));
filter('countProject2', () => sumByType(1))
全lamda函数写,看起来就很爽
这里用到的就是curry化。sumByType函数是curried function。部分调用后产生偏函数(partial)。解决了重复的问题。当然这个sumByType也叫高阶函数(HOF)。
一点总结
javascript在函数式上只是稍具意味,属于东拼西凑,需要搭配第三方库来配合使用。使用javascript时应该综合考虑。不应盲目使用编程范式。但是相信之后函数式编程范式在javascipt上会得到增强。