函数式编程
函数式编程思想主要内容:
- curry
- 高阶函数
- 递归
- 纯函数
- 流编程-pipe/compose
- 无类编程
- Container、functor-Maybe/IO/Task/Either-of/map/
- Monad-join/chain/mcompose
- Applicative Functor-ap
学习方法:
- 库:目前有lodash、lodash/fp和ramda两个库,补充库有:futil-js
- 基本概念,看书。
- 如何使用,看库API。
- 实践-用起来,看项目例子,多多使用函数式编程思想指导尝试优化代码。
- 总结,看书,回归概念。
- 提升,再次带着概念看代码并尝试优化。
函数式编程核心思想-curry:
为什么要使用curry,例子:
不使用curry时带来的问题,引入为什么要使用curry,curry有什么好处?
// 不使用curry,代码长,且可读性不够好,不够语义化,想想html的语义化,这就是使用curry的其中一个目的。
var objects = [{ id: 1 }, { id: 2 }, { id: 3 }]
objects.map(function(o){ return o.id })
// 多个属性调用时,需要写多个map函数。
// 使用curry
var get = curry(function(property, object){ return object[property] })
objects.map(get('id')) //= [1, 2, 3]
// 多个属性调用时,只需要确定属性即可。但代码不够语义化。
// 更加语义化
var getIDs = function(objects){
return objects.map(get('id'))
}
getIDs(objects) //= [1, 2, 3]
// 但id写死了,耦合度高。
// 使用curry,解耦属性指定,函数可以灵活使用。
var map = curry(function(fn, value){ return value.map(fn) })
var getIDs = map(get('id'))
getIDs(objects) //= [1, 2, 3]
// 语义化,并且不会耦合。
// 上面的推导很有意思,可以反复琢磨,让自己习惯函数式编程的思维。
// 由于旧的编程思想已经形成了编程习惯,平时写代码时会习惯性的编写非函数式编程的代码。所以,我们需要经常练习并养成函数式编程思想的编码习惯。
// 对比日常处理数据时,不使用curry和使用curry的写法:
// 假设有请求到这样的数据:
let data = {
"user": "hughfdjackson",
"posts": [
{ "title": "why curry?", "contents": "..." },
{ "title": "prototypes: the short(est possible) story", "contents": "..." }
]
}
// 想要对这个数据进行一系列的操作
// 一般的写法
fetchFromServer()
.then(JSON.parse)
.then(function(data){ return data.posts })
.then(function(posts){
return posts.map(function(post){ return post.title })
})
// 使用curry的写法,简洁而且语义清晰,代码逻辑和可读性强
fetchFromServer()
.then(JSON.parse)
.then(get('posts'))
.then(map(get('title')))
爱上curry-例子:(对比使用前后区别,突出curry优势)
// 假设请求到数据:
var data = {
result: "SUCCESS",
interfaceVersion: "1.0.3",
requested: "10/17/2013 15:31:20",
lastUpdated: "10/16/2013 10:52:39",
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"}
// , ...
]
};
// 一般写法:
getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return data.tasks;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (tasks[i].username == membername) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (!tasks[i].complete) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [], task;
for (var i = 0, len = tasks.length; i < len; i++) {
task = tasks[i];
results.push({
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
})
}
return results;
})
.then(function(tasks) {
tasks.sort(function(first, second) {
var a = first.dueDate, b = second.dueDate;
return a < b ? -1 : a > b ? 1 : 0;
});
return tasks;
});
};
// curry的写法:
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(R.get('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.reject(R.propEq('complete', true)))
.then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
.then(R.sortBy(R.get('dueDate')));
};
函数式指北-例子:
var curry = require('lodash').curry;
// 将原函数转化为curry函数
var match = curry(function(what, str) {
return str.match(what);
});
var replace = curry(function(what, replacement, str) {
return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
return ary.filter(f);
});
var map = curry(function(f, ary) {
return ary.map(f);
});
// 生成中间函数
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
// 1. 可以直接使用
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
// 2. 生成的中间函数,作为其他函数的参数使用
filter(hasSpaces, ["tori_spelling", "tori amos"]);
// ["tori amos"]
// 使用curry函数和中间函数生成更加具体的语义化函数
var findSpaces = filter(hasSpaces);
// function(xs) { return xs.filter(function(x) { return x.match(/\s+/g) }) }
findSpaces(["tori_spelling", "tori amos"]);
// ["tori amos"]
// 生成语义化函数
var noVowels = replace(/[aeiou]/ig);
// function(replacement, x) { return x.replace(/[aeiou]/ig, replacement) }
// 第二次传参,生成具体语义化函数
var censored = noVowels("*");
// function(x) { return x.replace(/[aeiou]/ig, "*") }
censored("Chocolate Rain");
// 'Ch*c*l*t* R**n'
函数式指北-一对多转换
var getChildren = function(x) {
return x.childNodes;
};
var allTheChildren = map(getChildren);
函数式指北-局部调用
只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码
// 预先设定一个或几个参数,参数的顺序是反过来的,即从参数顺序的后面开始入参。
var allTheChildren = function(elements) {
return _.map(elements, getChildren);
};
// 库写法
// lodash库:
var allTheChildren = _.partial(_.map, getChildren) // 反转参数顺序
// ramda库:
var allTheChildren = R.flip(R.map)(getChildren) // 反转参数顺序,并调用第一个参数。
var allTheChildren = R.map(R.__, getChildren) // 使用R.__占位符,下次传参时会把参数放到占位符的位置上。
// 上面几种写法意思都是反转参数顺序,效果相同
其他有趣的事情
一个函数通过传入不同的值而生成不同的功能函数
// curry的一个有趣应用
// 生成一个二次方程函数
var quadratic = R.curry((a, b, c, x) => x * x * a + x * b + c)
// (a, 0, 0, x) => x * x * a + x * 0 + 0
// (1, 0, 0, x) => 2 * 2
// (1, 0, 0, x) => x ^ 2
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> 4
// 生成一个偏移量函数
// (0, 1, -1, x) => x - 1
var xOffset = quadratic(0, 1, -1)
xOffset(0); //=> -1
xOffset(1); //=> 0
函数式指北-curry练习
var _ = require('ramda');
// 练习 1
//==============
// 通过局部调用(partial apply)移除所有参数
var words = function(str) {
return split(' ', str);
};
// 练习 1a
//==============
// 使用 `map` 创建一个新的 `words` 函数,使之能够操作字符串数组
var sentences = undefined;
// 练习 2
//==============
// 通过局部调用(partial apply)移除所有参数
var filterQs = function(xs) {
return filter(function(x){ return match(/q/i, x); }, xs);
};
// 练习 3
//==============
// 使用帮助函数 `_keepHighest` 重构 `max` 使之成为 curry 函数
// 无须改动:
var _keepHighest = function(x,y){ return x >= y ? x : y; };
// 重构这段代码:
var max = function(xs) {
return reduce(function(acc, x){
return _keepHighest(acc, x);
}, -Infinity, xs);
};
// 彩蛋 1:
// ============
// 包裹数组的 `slice` 函数使之成为 curry 函数
// //[1,2,3].slice(0, 2)
var slice = undefined;
// 彩蛋 2:
// ============
// 借助 `slice` 定义一个 `take` curry 函数,该函数调用后可以取出字符串的前 n 个字符。
var take = undefined;
curry总结:
技术上:通过闭包暂存参数,从而生成语义化函数。
定义:每次入参都会生成一个函数用于处理剩余的参数,curry函数会一直消耗参数,直到最后一次入参后,运行函数得到结果。
特点:
- curry函数是纯函数
- 可以直接调用
- 作为其他函数的参数使用
- 函数构建函数,使函数语义化
- 多次curry多用途,即中间函数可以被用作他途。
- 可以简洁的处理一对多的场景,并且语义清晰。
- 局部柯里化被Ramda库通过倒置参数实现控制反转的整体构建思想消灭掉了,虽然库里面还是保留了参数倒置API