javaScript之函数组合

函数组合

需求

我们需要写一个函数,输入“blue”,返回“HELLO BLUE ”。

尝试
1
2
3
4
5
6
var toUpperCase = function(x) {return x.toUpperCase();};
var hello = function(x) {return "HELLO, " + x;}
var greet = function(x) {
return hello(toUpperCase(x));
};
greet('blue')

目前的步骤较少,所以嵌套的不是很深,如果有更多的操作,greet函数就要更多的嵌套,就类似与于fn3(fn2(fn1(fn0(x))))了。下面就来进行优化

优化

这里写一个compose函数:

1
2
3
4
5
6
7
8
9
   var compose = function(f,g) {
return function(x) {
return f(g(x))
};
};
//greet 函数就可以被优化成

var greet = compose(hello, toUpperCase);
greet('blue')

利用compose将两个函数组合成一个函数,让代码从右往左运行,而不是由内而外运行,可读性大大提升。者就是函数组合。
但是现在compose函数也只是能支持两个参数,如果有更多的步骤呢?最后好像和前面说的嵌套又没有什么区别了compose(d, compose(c, compose(b, a)))

compose

这里是underscore的compose函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 var toUppCase = function(x) {return x.toUpperCase()}
var hello = function(x) {return "hello " + x;}

function compose(){
// 传递进来的各个函数s
var args = arguments;
var start = args.length - 1;

return function() {

var i = start;
//从后往前
var result = args[start].apply(this, arguments);
while (i --) result = args[i].call(this, result);
return result;
}
}
// 使用
var s = compose(toUppCase, hello);
console.log(s('blue'))

pointfree

这里我们来了解一个概念叫做pointfree

pointfree指的是函数无须提及将要操作的数据是什么样的。

以最初的需求为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 需求: 输入'blue',返回 'HELLO BLUE'。
// 非 pointfree,因为提及到了数据:name
var greet = function(name) {
return ('hello ' + name).toUpperCase();
}

// pointfree
// 先定义基本运算,这些可以封装起来复用
var toUpperCase = function(x) {return x.toUpperCase();};
var hello = function(x) {return 'HELLO, ' + x};

var greet = compose(hello, toUpperCase);
greet('blue');

这里结合curry函数写一个更加复杂的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 需求: 输入'blue is good man', 返回 'B.I.G.M'
// 非 pointfree,因为提到了数据:name
var initials = function(name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

// pointfree
//先定义基本运算
var split = curry(function(separator, str) {return str.split(separator)})
var head = function(str) {return str.slice(0, 1)}
var join = curry(function(separator, arr) { return arr.join(separator)})
var map = curry(function(fn, arr) {return arr.map(fn)})

var initials = compose(join('. '), map(compose(toUpperCase, head)), split(" "));

initials('blue is a good man');

从这个例子中我们可以看到,利用柯里化(柯里化)和函数组合(compose)非常有助于实现pointfree

尽管感觉这个东西好像是很厉害的样子,但是写起来真是麻烦,还要写这么多的基础函数…..,但是如果有工具库已经帮你写好了呢?比如ramda.js

1
2
// 使用 ramda.js
var initials = R.compose(R.join('.'), R.map(R.compose(R.toUpper, R.head)), R.split(' '));

而且你也会发现:

Pointfree的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不适用所要处理的值,只合成运算过程。

那么使用pointfree模式究竟有什么好处呢?

pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举。

实战

例子来源

假设我们从服务器获取这样的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var data = {
result: "SUCCESS",
tasks: [
{id: 104, complete: false, priority: "high",
dueDate: "2013-11-29", username: "Scott",
title: "Do something", created: "9/22/2013"},
{id: 105, complete: false, priority: "medium",
dueDate: "2013-11-22", username: "Lena",
title: "Do something else", created: "9/22/2013"},
{id: 107, complete: true, priority: "high",
dueDate: "2013-11-22", username: "Mike",
title: "Fix the foo", created: "9/22/2013"},
{id: 108, complete: false, priority: "low",
dueDate: "2013-11-15", username: "Punam",
title: "Adjust the bar", created: "9/25/2013"},
{id: 110, complete: false, priority: "medium",
dueDate: "2013-11-15", username: "Scott",
title: "Rename everything", created: "10/2/2013"},
{id: 112, complete: true, priority: "high",
dueDate: "2013-11-27", username: "Lena",
title: "Alter all quuxes", created: "10/5/2013"}
]
};

下面使用到的curry函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sub_curry(fn) {
var args = [].slice.call(arguments, 1);
return function() {
return fn.apply(this, args.concat([].slice.call(arguments)));
};
}

function curry(fn, length) {
//fn.length是fn的参数长度
length = length || fn.length;
var slice = Array.prototype.slice;

return function() {
//通过判断参数的长度来决定是否退出递归
if(arguments.length < length) {
var combined = [fn].concat(slice.call(arguments));
// length-arguments.length 来判断是否有二次参数传递
return curry(sub_curry.apply(this, combined), length - arguments.length);
}else {
return fn.apply(this, arguments);
}
};
}

我们需要写一个 getIncompleteTaskSummaries 的函数,接收一个 username 作为参数,从服务器获取数据,然后筛选出这个用户的未完成的任务的 ids、priorities、titles、和 dueDate 数据,并且按照日期升序排序。
Scott 为例,最终筛选出的数据为:

1
2
3
4
5
6
[
{id: 110, title: "Rename everything",
dueDate: "2013-11-15", priority: "medium"},
{id: 104, title: "Do something",
dueDate: "2013-11-29", priority: "high"}
]

普通方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 第一版 过程式编程
var fetchData = function() {
// 模拟
return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return data.tasks;
})
.then(function(tasks) {
return tasks.filter(function(task) {
return task.username == membername
})
})
.then(function(tasks) {
return tasks.filter(function(task) {
return !task.complete
})
})
.then(function(tasks) {
return tasks.map(function(task) {
return {
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
}
})
})
.then(function(tasks) {
return tasks.sort(function(first, second) {
var a = first.dueDate,
b = second.dueDate;
return a < b ? -1 : a > b ? 1 : 0;
});
})
.then(function(task) {
console.log(task)
})
};

getIncompleteTaskSummaries('Scott')

如果使用 pointfree 模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 第二版 pointfree改写
var fetchData = function() {
return Promise.resolve(data);
};
// 编写基本函数
var prop = curry(function(name, obj) {
return obj[name];
})

var propEq = curry(function(name, val, obj) {
return obj[name] === val;
});

var filter = curry(function(fn, arr) {
return arr.filter(fn);
});

var map = curry(function(fn, arr) {
return arr.map(fn);
});

var pick = curry(function(args, obj) {
var result = {};
for(var i = 0; i < args.length; i++){
result[args[i] = obj[args[i]]];
}
return result;
})

var sortBy = curry(function(fn, arr) {
return arr.sort(function(a, b) {
var a = fn(a),
b = fn(b);
return a < b ? -1 : a > b ? 1 : 0;
})
});

var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(prop('task'))
.then(filter(propEq('username', memebername)))
.then(filter(propEq('complete', false)))
.then(map(pick(['id', 'dueDate', 'title', 'priority'])))
.then(sortBy(prop('dueDate')))
.then(console.log)
}

getIncompleteTaskSummaries('Scott')

如果直接使用 ramda.js,你可以省去编写基本函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//第三版使用 ramda.js
var fetchData = function() {
return Promise.resolve(data)
};

var getIncompleteTaskSumaries = functin(membername) {
return fetchData()
.then(R.prop('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.filter(R.propEq('complete', false)))
.then(R.sortBy(R.prop('dueDate')))
.then(console.log)
};

getIncompleteTaskSummaries('Scott');

当然了,利用 compose,你也可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第四版 使用 compose 
fetchData = function() {
return Promise.resolve(data)
};


var getIncompleteTaskSummaries = function(memebername) {
return fetchData()
//执行顺序是从下往上
.then(R.compose(
console.log,
R.sortBy(R.prop('dueDate')),
R.map(R.pick(['id', 'dueDate', 'title', 'priority'])),
R.filter(R.propEq('complete', false)),
R.filter(R.propEq('username', membername)),
R.prop('tasks'),
))
};

compose 是从右到左依此执行,当然你也可以写一个从左到右的版本,但是从右向左执行更加能够反映数学上的含义。

ramda.js 提供了一个 R.pipe 函数,可以做到从左到右,以上可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
// 第五版 使用 R.pipe
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(R.pipe(
R.prop('task'),
R.filter(R.propEq('username', membername)),
R.filter(R.propEq('complete', false)),
R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
console.log,
)
))
};