JavaScript: 函数式编程 - 代码组合的优势

4,307 阅读5分钟

了解JavaScript函数式编程目录

代码组合

养殖代码

组合函数看起来像是在搭积木。你就是一个孩子,可以随意选择两个积木(函数),让它们拼接(结合)一下,拼接成一个新的玩具(函数)。组合的用法如下:

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

fg 都是函数,x 是在它们之间通过“管道”传输的值。这里得注意一下 compose 函数是组合代码思想中最重要的一环,下面👇会经常用到。大家可以提前 mark 一下

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"

就像这样我们把两个函数组合之后返回一个新的函数。

对比组合函数

思考一下: 组合的函数和一个完整流程的函数有什么区别
var shout = function(x){
  return exclaim(toUpperCase(x));
};

可以看到组合的函数就像乐高玩具一样可以自由的组合成其他的可用的完整的模型乐高建模(完整功能的函数),但是一个像上方一样完整功能,不可拆卸对的函数的。它就像一个已经完成的手办。

- 可以拆解,组合成其他的乐高模型
  • 不可拆解,出厂的时候已经设计好了。

了解组合代码

让代码从右向左运行,而不是由内而外运行,我觉得可以称之为“左派”(消音~)。我们来看一个顺序很重要的例子:

var curry = require("lodash").curry;
var reduce = curry(function(f, init, arr){
    return arr.reduce(f, init);
});

// 感谢掘友(@thsing772)提示,这里勘误一下 reduce 函数,应该是需要先 curry 处理一下,才能如下使用。
var head = function(x) { return x[0]; };
var reverse = reduce(function(acc, x){ return [x].concat(acc); }, []);
// 当然你也可以不使用 curry
// var reverse = x => x.reduce(function (arr,x){return [x].concat(ar)}[]);
var last = compose(head, reverse);

last(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'uppercut'

上面就是一个反转数组的操作,这里我们看到一个组合函数的执行顺利,我们可以主动的操作函数进行从左到右的方式,但是从右向左的执行顺序更加符合数学上的含义。基本的高中数学知识

// 结合律(associativity)
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true

compose(toUpperCase, compose(head, reverse));

// 或者
compose(compose(toUpperCase, head), reverse);

结合律的好处

结合律的好处是任何一个函数分组都可以被拆解开来,然后再以他们自己的组合打包在一起,组合成新的函数。curry 就是我们的工具包。

  • 下面用到了上面 compose 、head、reverse 函数
var loudLastUpper = compose(exclaim, toUpperCase, head, reverse);

// 或
var last = compose(head, reverse);
var loudLastUpper = compose(exclaim, toUpperCase, last);

// 或
var last = compose(head, reverse);
var angry = compose(exclaim, toUpperCase);
var loudLastUpper = compose(angry, last);

// 更多变种...

pointfree 空数据模式

pointfree 模式是,no data 模式的意思。这里有一句来自《Love Story》70 年代的电影的台词--“Love means never having to say you're sorry”。

  • 我们的 pointfree 模式就是“Pointfree style means never to say your data”。
  • 下面我们就可以使用柯里化、组合代码中实现以下 pointfree style
// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

// 不明白为什么看看最上面的 compose 函数,然后在 控制台试试 snakeCase 函数

snakeCase('Hello World')
// hello_world

在 pointfree 版本中,不需要 word 参数就能构造函数;而在非 pointfree 的版本中,必须要有 word 才能进行一切操作。

pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。

组合代码的常见问题 debug~

组合的一个常见错误是,在没有局部调用之前,就组合类似 map 这样接受两个参数的函数。

// 下面部分函数来自于上面的 #### 结合律的好处

// 错误做法:我们传给了 `angry` 一个数组,根本不知道最后传给 `map` 的是什么东西。
var latin = compose(map, angry, reverse);

latin(["frog", "eyes"]);
// error


// 正确做法:每个函数都接受一个实际参数。
var latin = compose(map(angry), reverse);

latin(["frog", "eyes"]);
// ["EYES!", "FROG!"])

使用 trace(追踪) 来跟踪你的函数

var trace = curry(function(tag, x){
  console.log(tag, x);
  return x;
});

var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined

--------------

// 看到报错了,来 trace 一下
var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' '));
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]

-------------

// tolower 的参数徐亚的是一个数组,所以这里我们 fix 一下我们的代码
var dasherize = compose(join('-'), map(toLower), split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');

// 'the-world-is-a-vampire'

trace 的好处就是可以直接定位到函数调用的地方,允许我们在某个特定的点去观察我们的函数数据

总结

我们可以认为哦组合是高于其他所有原则的设计原则,这是因为组合让我们的代码简单而富有可读性。另外范畴学将在应用架构、模拟副作用和保证正确性方面扮演重要角色。

衍生知识点:一些命名方式

namedemo
CamelCase(小驼峰)personId
PascalCase(帕斯卡/大驼峰)PersonId
SnakeCase(下横线)person_id
KebabCase(中横线)person-id

参考