# functional-programming **Repository Path**: JXHuang_admin/functional-programming ## Basic Information - **Project Name**: functional-programming - **Description**: 函数式编程 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-10-24 - **Last Updated**: 2023-09-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 函数式编程 ## 函数式编程思想主要内容: - curry - 高阶函数 - 递归 - 纯函数 - 流编程-pipe/compose - 无类编程 - Container、functor-Maybe/IO/Task/Either-of/map/ - Monad-join/chain/mcompose - Applicative Functor-ap ## 学习方法: 1. 库:目前有lodash、lodash/fp和ramda两个库,补充库有:[futil-js][1] 2. 基本概念,看书。 3. 如何使用,看库API。 4. 实践-用起来,看项目例子,多多使用函数式编程思想指导尝试优化代码。 5. 总结,看书,回归概念。 6. 提升,再次带着概念看代码并尝试优化。 ## 函数式编程核心思想-curry: 1. [为什么要使用curry][2] 2. [爱上柯里化][3] 3. [函数式指北-柯里化][4] ### 为什么要使用curry,例子: 不使用curry时带来的问题,引入为什么要使用curry,curry有什么好处? ```js // 不使用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优势) ```js // 假设请求到数据: 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'))); }; ``` ### 函数式指北-例子: ```js 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' ``` ### 函数式指北-一对多转换 ```js var getChildren = function(x) { return x.childNodes; }; var allTheChildren = map(getChildren); ``` ### 函数式指北-局部调用 只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码 ```js // 预先设定一个或几个参数,参数的顺序是反过来的,即从参数顺序的后面开始入参。 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.__占位符,下次传参时会把参数放到占位符的位置上。 // 上面几种写法意思都是反转参数顺序,效果相同 ``` ### 其他有趣的事情 一个函数通过传入不同的值而生成不同的功能函数 ```js // 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练习 ```js 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 [1]: https://github.com/smartprocure/futil-js [2]: http://hughfdjackson.com/javascript/why-curry-helps/ [3]: https://adispring.github.io/2017/06/27/Favoring-Curry/ [4]: https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch4.html