先介绍一个用于函数式编程的库:Ramda 下面我们会用到很多这个库的函数
函数式编程其中几个重要概念
- 函数是一等公民(函数可以当作参数随意传递)
- 只用表达式,不用语句(只表明我们要做什么,不需要在意怎么做)
举个简单的🌰
// 我们循环一个数字列表,并为每一项的值加1var arr = [1, 2, 3];// 语句式的写法for (var i = 0; i < arr.length; i++) {arr[i] += 1;}console.log(arr); // [2, 3, 4]// 表达式写法const newArr = arr.map(function(item) {return item + 1;})console.log(newArr); // [2, 3, 4]
- 函数式编程的思想是把运算过程尽量写成一系列嵌套的函数调用
- 所有的函数都要有返回值
简单的例子
接下来我们遵循这三个原则先来写一个简单的例子
// 我们定义两个函数var add = function(x) {return x + 1;}var multiply = function(x) {return x * 2;}
假如我们要计算 (2+1)*2 的结果,利用这两个函数的写法如下
multiply(add(2)); // 6
虽然现在看上去有点傻,你也许会说我直接 var a = (2+1)*2-1; 它不香吗?
但是我们只是为了表达函数式编程的思路,是为了应用到更复杂的业务中。
虽然想象很美好,但是上面这种写法确实有点不太优雅,如果业务过于复杂,那就会像下面这样
multiply(add(multiply(add(10)))); // ...
下面就为大家介绍第一个工具来摆脱这种看了就头疼的写法
函数组合 compose
// 我们定义两个函数var add = function(x) {return x + 1;}var multiply = function(x) {return x * 2;}// 这里只是简化版,我们可以应用 ramda 为我们提供的函数 ramda.compose// https://ramda.cn/docs/#composevar compose = function (f, g) {return function (x) {return f(g(x));};};
我们来用 compose 来改写一下上面的代码
var calc = compose(multiply, add);calc(2); // 6var clac2 = compose(multiply, add, multiply, add);clac2(10); // 22
现在是不是清晰多了
柯里化 curry
但是还存在一个问题,不可能所有的函数都只有一个参数啊
如果上面的 add 和 multiply 都有两个参数怎么办?
var add = function(x, y) {return x + y;}var multiply = function(x, y) {return x * y;}
接下来我们就会用 curry 来解决这一问题
var R = require('ramda');var add = R.curry(function(y, x) {return x + y;})var multiply = R.curry(function(y, x) {return x * y;})
先来看一下 curry 的作用
add(1)(2); // 3add(1, 2); // 3
curry 将参数拆分开了,我们可以决定分开传,或者同时传
然后我们再来实现 (2+1)*2 这个例子
var calc = R.compose(multiply(2), add(1)); // (2+1)*2calc(2); // 6var calc2 = R.compose(multiply(5), add(5)); // (2+5)*5calc2(2); // 35
下面我们通过一些例子来进一步了解 curry,以便让我们知道在真实场景中应该怎么用它。
var curry = require('lodash').curry;var filter = curry(function(f, ary) {return ary.filter(f);});var map = curry(function(f, ary) {return ary.map(f);});
我在上面的代码中遵循的是一种简单,同时也非常重要的模式。即策略性地把要操 作的数据(String, Array)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。
var myFilter = filter(v => v < 10);myFilter([67, 9, 1, 17, 0, -1]); // [ 9, 1, 0, -1 ]myFilter([19, 7, 11, 77, 7, 8]); // [ 7, 7, 8 ]var getChildren = function(x) {return x.childNodes;};var allTheChildren = map(getChildren);allTheChildren([{childNodes: {...}}]); // [{...}]
这样我们就可以随意组合,并且没有任何副作用。
我们在利用 compose 实现一个完整的例子加加深一下印象
var R = require('ramda');var map = R.curry(function(f, ary) {return ary.map(f);});var getHead = function(arr) {return arr[0]}var getUserNames = function(x) {return x.name;};const getFirstName = R.compose(getHead, map(getUserNames));getFirstName([{name: "for."}, {name: "Limi"}]); // for.
注意⚠️ 在组合像 map 这种多参数的函数时,需要确保每个参数都能正确传递
如果在组合函数执行时遇到来错误怎么办?下面我们来说一下如何进行 debug。
debug
还拿上面的代码为例
var trace = curry(function(tag, x){console.log(tag, x);return x;});// 假如我们想看一下 map 的返回值时什么const getFirstName = R.compose(getHead, trace("map result:"), map(getUserNames));getFirstName([{name: "for."}, {name: "Limi"}]);// map result: [ 'for.', 'Limi' ]
完整的 Demo
下面我们来实现一个完整的 demo
从 html 开始
./index.html
<!DOCTYPE html><html><head><script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script><script src="flickr.js"></script> </head><body></body></html>
./flickr.js
requirejs.config({paths: {ramda: 'https://cdn.bootcdn.net/ajax/libs/ramda/0.27.0/ramda.min',jquery: 'https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min'}});require(['ramda', 'jquery'],function (_, $) {var trace = _.curry(function (tag, x) {console.log(tag, x);return x;});});
我们用 requirejs 实现模块加载我们需要的库 ramda。
也事先写好了 trace 函数用于 debug
接下来我们来实现一个简单的接口请求,查询某班级的学生,并打印学生名字的过程。
var Impure = {// 这里模拟一个假的请求getJSON: _.curry(function(callback, url) {const data = {code: "200", data: [{name: "LiMing", age: 18},{name: "Ageo", age: 18}]};setTimeout(() => callback(data), 200);}),};var url = function (id) {return 'https://api.xxx.com/api/getStudent?classRoomId=' + id;};var getName = o => o.name;var consoleName = _.compose(trace("students name:"), _.map(getName), _.prop("data"))var getJson = _.compose(Impure.getJSON(consoleName), url);getJson("xxx");// students name: (2) ["LiMing", "Ageo"]s
完整代码
requirejs.config({paths: {ramda: 'https://cdn.bootcdn.net/ajax/libs/ramda/0.27.0/ramda.min',}});require(['ramda'],function (_, $) {var trace = _.curry(function (tag, x) {console.log(tag, x);return x;});var Impure = {// 这里模拟一个假的请求getJSON: _.curry(function (callback, url) {const data = { code: "200", data: [{ name: "LiMing", age: 18 }, { name: "Ageo", age: 18 }] };setTimeout(() => callback(data), 200);}),};var url = function (id) {return 'https://api.xxx.com/api/getStudent?classRoomId=' + id;};var getName = o => o.name;var consoleName = _.compose(trace("students name:"), _.map(getName), _.prop("data"))var getJson = _.compose(Impure.getJSON(consoleName), url);getJson("xxx");});
类型签名 Hindley-Milner
**
类型签名其实就像我们平常写的注释一样,表明了函数的参数及类型是什么,返回值是什么
先来看一个最简单的签名
// add :: Number -> Numberfunction add(x) {return x + 1;}// strLength :: String -> Numbervar strLength = function(s){return s.length;}// join :: String -> [String] -> Stringvar join = curry(function(what, xs){return xs.join(what);});join(",")(["Limi", "Ageo"]); // Limi,Ageo// match :: Regex -> String -> [String]var match = curry(function(reg, s){return s.match(reg);});// replace :: Regex -> String -> String -> Stringvar replace = curry(function(reg, sub, s){return s.replace(reg, sub);});
当参数的类型不那么明确时
() 代表一个函数
// id :: a -> avar id = function(x){ return x; }// map :: (a -> b) -> [a] -> [b]var map = curry(function(f, xs){return xs.map(f);});
这里的 id 函数接受任意类型的 a 并返回同一个类型的数据。和普通代码一 样,我们也可以在类型签名中使用变量。把变量命名为 a 和 b 只是一种约定俗 成的习惯,你可以使用任何你喜欢的名称。对于相同的变量名,其类型也一定相 同。这是非常重要的一个原则,所以我们必须重申: a -> b 可以是从任意类型 的 a 到任意类型的 b ,但是 必须是同一个类型。例如, id 可以是 String -> String,也可以是 ,但不能是 String -> Bool。
试着理解下面的签名
// head :: [a] -> avar head = function(xs){return xs[0];}// filter :: (a -> Bool) -> [a] -> [a]var filter = curry(function(f, xs){return xs.filter(f);});// reduce :: (b -> a -> b) -> b -> [a] -> b// reduce 函数说明 https://www.runoob.com/jsref/jsref-reduce.htmlvar reduce = curry(function(f, x, xs){return xs.reduce(f, x);});
范畴学
好玩儿的学完了,下面来学一些理论知识吧
理理解函数式编程的关键,就是理理解范畴论。它是⼀一⻔门很复杂的数学,认为世界上所有的概念体系,都可 以抽象成⼀一个个的”范畴”(category)。
维基百科给出的解释:”范畴就是使⽤用箭头连接的物体。”
简单点理解就是,所有的事物,只要他们之间有一点点联系,就可以将他们定义为一个范畴
