先介绍一个用于函数式编程的库:Ramda 下面我们会用到很多这个库的函数

函数式编程其中几个重要概念

  1. 函数是一等公民(函数可以当作参数随意传递)
  2. 只用表达式,不用语句(只表明我们要做什么,不需要在意怎么做)

举个简单的🌰

  1. // 我们循环一个数字列表,并为每一项的值加1
  2. var arr = [1, 2, 3];
  3. // 语句式的写法
  4. for (var i = 0; i < arr.length; i++) {
  5. arr[i] += 1;
  6. }
  7. console.log(arr); // [2, 3, 4]
  8. // 表达式写法
  9. const newArr = arr.map(function(item) {
  10. return item + 1;
  11. })
  12. console.log(newArr); // [2, 3, 4]
  1. 函数式编程的思想是把运算过程尽量写成一系列嵌套的函数调用
  2. 所有的函数都要有返回值

简单的例子

接下来我们遵循这三个原则先来写一个简单的例子

  1. // 我们定义两个函数
  2. var add = function(x) {
  3. return x + 1;
  4. }
  5. var multiply = function(x) {
  6. return x * 2;
  7. }

假如我们要计算 (2+1)*2 的结果,利用这两个函数的写法如下

  1. multiply(add(2)); // 6

虽然现在看上去有点傻,你也许会说我直接 var a = (2+1)*2-1; 它不香吗?
但是我们只是为了表达函数式编程的思路,是为了应用到更复杂的业务中。

虽然想象很美好,但是上面这种写法确实有点不太优雅,如果业务过于复杂,那就会像下面这样

  1. multiply(add(multiply(add(10)))); // ...

下面就为大家介绍第一个工具来摆脱这种看了就头疼的写法

函数组合 compose

  1. // 我们定义两个函数
  2. var add = function(x) {
  3. return x + 1;
  4. }
  5. var multiply = function(x) {
  6. return x * 2;
  7. }
  8. // 这里只是简化版,我们可以应用 ramda 为我们提供的函数 ramda.compose
  9. // https://ramda.cn/docs/#compose
  10. var compose = function (f, g) {
  11. return function (x) {
  12. return f(g(x));
  13. };
  14. };

我们来用 compose 来改写一下上面的代码

  1. var calc = compose(multiply, add);
  2. calc(2); // 6
  3. var clac2 = compose(multiply, add, multiply, add);
  4. clac2(10); // 22

现在是不是清晰多了

柯里化 curry

但是还存在一个问题,不可能所有的函数都只有一个参数啊
如果上面的 add 和 multiply 都有两个参数怎么办?

  1. var add = function(x, y) {
  2. return x + y;
  3. }
  4. var multiply = function(x, y) {
  5. return x * y;
  6. }

接下来我们就会用 curry 来解决这一问题

  1. var R = require('ramda');
  2. var add = R.curry(function(y, x) {
  3. return x + y;
  4. })
  5. var multiply = R.curry(function(y, x) {
  6. return x * y;
  7. })

先来看一下 curry 的作用

  1. add(1)(2); // 3
  2. add(1, 2); // 3

curry 将参数拆分开了,我们可以决定分开传,或者同时传
然后我们再来实现 (2+1)*2 这个例子

  1. var calc = R.compose(multiply(2), add(1)); // (2+1)*2
  2. calc(2); // 6
  3. var calc2 = R.compose(multiply(5), add(5)); // (2+5)*5
  4. calc2(2); // 35

下面我们通过一些例子来进一步了解 curry,以便让我们知道在真实场景中应该怎么用它。

  1. var curry = require('lodash').curry;
  2. var filter = curry(function(f, ary) {
  3. return ary.filter(f);
  4. });
  5. var map = curry(function(f, ary) {
  6. return ary.map(f);
  7. });

我在上面的代码中遵循的是一种简单,同时也非常重要的模式。即策略性地把要操 作的数据(String, Array)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。

  1. var myFilter = filter(v => v < 10);
  2. myFilter([67, 9, 1, 17, 0, -1]); // [ 9, 1, 0, -1 ]
  3. myFilter([19, 7, 11, 77, 7, 8]); // [ 7, 7, 8 ]
  4. var getChildren = function(x) {
  5. return x.childNodes;
  6. };
  7. var allTheChildren = map(getChildren);
  8. allTheChildren([{childNodes: {...}}]); // [{...}]

这样我们就可以随意组合,并且没有任何副作用。
我们在利用 compose 实现一个完整的例子加加深一下印象

  1. var R = require('ramda');
  2. var map = R.curry(function(f, ary) {
  3. return ary.map(f);
  4. });
  5. var getHead = function(arr) {
  6. return arr[0]
  7. }
  8. var getUserNames = function(x) {
  9. return x.name;
  10. };
  11. const getFirstName = R.compose(getHead, map(getUserNames));
  12. getFirstName([{name: "for."}, {name: "Limi"}]); // for.

注意⚠️ 在组合像 map 这种多参数的函数时,需要确保每个参数都能正确传递

如果在组合函数执行时遇到来错误怎么办?下面我们来说一下如何进行 debug。

debug

还拿上面的代码为例

  1. var trace = curry(function(tag, x){
  2. console.log(tag, x);
  3. return x;
  4. });
  5. // 假如我们想看一下 map 的返回值时什么
  6. const getFirstName = R.compose(getHead, trace("map result:"), map(getUserNames));
  7. getFirstName([{name: "for."}, {name: "Limi"}]);
  8. // map result: [ 'for.', 'Limi' ]

完整的 Demo

下面我们来实现一个完整的 demo
从 html 开始

./index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script>
  5. <script src="flickr.js"></script> </head>
  6. <body></body>
  7. </html>

./flickr.js

  1. requirejs.config({
  2. paths: {
  3. ramda: 'https://cdn.bootcdn.net/ajax/libs/ramda/0.27.0/ramda.min',
  4. jquery: 'https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min'
  5. }
  6. });
  7. require(['ramda', 'jquery'],
  8. function (_, $) {
  9. var trace = _.curry(function (tag, x) {
  10. console.log(tag, x);
  11. return x;
  12. });
  13. }
  14. );

我们用 requirejs 实现模块加载我们需要的库 ramda。
也事先写好了 trace 函数用于 debug

接下来我们来实现一个简单的接口请求,查询某班级的学生,并打印学生名字的过程。

  1. var Impure = {
  2. // 这里模拟一个假的请求
  3. getJSON: _.curry(function(callback, url) {
  4. const data = {code: "200", data: [{name: "LiMing", age: 18},{name: "Ageo", age: 18}]};
  5. setTimeout(() => callback(data), 200);
  6. }),
  7. };
  8. var url = function (id) {
  9. return 'https://api.xxx.com/api/getStudent?classRoomId=' + id;
  10. };
  11. var getName = o => o.name;
  12. var consoleName = _.compose(trace("students name:"), _.map(getName), _.prop("data"))
  13. var getJson = _.compose(Impure.getJSON(consoleName), url);
  14. getJson("xxx");
  15. // students name: (2) ["LiMing", "Ageo"]s

完整代码

  1. requirejs.config({
  2. paths: {
  3. ramda: 'https://cdn.bootcdn.net/ajax/libs/ramda/0.27.0/ramda.min',
  4. }
  5. });
  6. require(['ramda'],
  7. function (_, $) {
  8. var trace = _.curry(function (tag, x) {
  9. console.log(tag, x);
  10. return x;
  11. });
  12. var Impure = {
  13. // 这里模拟一个假的请求
  14. getJSON: _.curry(function (callback, url) {
  15. const data = { code: "200", data: [{ name: "LiMing", age: 18 }, { name: "Ageo", age: 18 }] };
  16. setTimeout(() => callback(data), 200);
  17. }),
  18. };
  19. var url = function (id) {
  20. return 'https://api.xxx.com/api/getStudent?classRoomId=' + id;
  21. };
  22. var getName = o => o.name;
  23. var consoleName = _.compose(trace("students name:"), _.map(getName), _.prop("data"))
  24. var getJson = _.compose(Impure.getJSON(consoleName), url);
  25. getJson("xxx");
  26. }
  27. );

类型签名 Hindley-Milner

**
类型签名其实就像我们平常写的注释一样,表明了函数的参数及类型是什么,返回值是什么

先来看一个最简单的签名

  1. // add :: Number -> Number
  2. function add(x) {
  3. return x + 1;
  4. }
  5. // strLength :: String -> Number
  6. var strLength = function(s){
  7. return s.length;
  8. }
  9. // join :: String -> [String] -> String
  10. var join = curry(function(what, xs){
  11. return xs.join(what);
  12. });
  13. join(",")(["Limi", "Ageo"]); // Limi,Ageo
  14. // match :: Regex -> String -> [String]
  15. var match = curry(function(reg, s){
  16. return s.match(reg);
  17. });
  18. // replace :: Regex -> String -> String -> String
  19. var replace = curry(function(reg, sub, s){
  20. return s.replace(reg, sub);
  21. });

当参数的类型不那么明确时

() 代表一个函数

  1. // id :: a -> a
  2. var id = function(x){ return x; }
  3. // map :: (a -> b) -> [a] -> [b]
  4. var map = curry(function(f, xs){
  5. return xs.map(f);
  6. });

这里的 id 函数接受任意类型的 a 并返回同一个类型的数据。和普通代码一 样,我们也可以在类型签名中使用变量。把变量命名为 a 和 b 只是一种约定俗 成的习惯,你可以使用任何你喜欢的名称。对于相同的变量名,其类型也一定相 同。这是非常重要的一个原则,所以我们必须重申: a -> b 可以是从任意类型 的 a 到任意类型的 b ,但是 必须是同一个类型。例如, id 可以是 String -> String,也可以是 ,但不能是 String -> Bool。

试着理解下面的签名

  1. // head :: [a] -> a
  2. var head = function(xs){
  3. return xs[0];
  4. }
  5. // filter :: (a -> Bool) -> [a] -> [a]
  6. var filter = curry(function(f, xs){
  7. return xs.filter(f);
  8. });
  9. // reduce :: (b -> a -> b) -> b -> [a] -> b
  10. // reduce 函数说明 https://www.runoob.com/jsref/jsref-reduce.html
  11. var reduce = curry(function(f, x, xs){
  12. return xs.reduce(f, x);
  13. });

范畴学

好玩儿的学完了,下面来学一些理论知识吧

理理解函数式编程的关键,就是理理解范畴论。它是⼀一⻔门很复杂的数学,认为世界上所有的概念体系,都可 以抽象成⼀一个个的”范畴”(category)。

维基百科给出的解释:”范畴就是使⽤用箭头连接的物体。”

简单点理解就是,所有的事物,只要他们之间有一点点联系,就可以将他们定义为一个范畴