JavaScript: 函数式编程 - 柯里化

2,795 阅读4分钟

了解JavaScript函数式编程目录

1、什么是 curry

curry

curry 就是咖喱一样美好的工具性的拌料让我们的函数更加的易用、低耦合性。

curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

curry 的重要性

在这个多彩的世界,有些事物对与我们来说并不是非必须的,就像我们早已习惯存在但是又非必须的东西:互联网,移动手机,微波炉,电梯等等。当他们不存在的时候我们也能正常的快乐的生存下去,但是一旦拥有了以后他们的存在就变得不可或缺。就像我们的 curry 工具一样。

  • 我们来创建一个普通的 curry ,for your enjoyment 吧。这里用到了 lodash 函数库,不熟悉的朋友可以看一下 lodash 的官网
var curry = require('lodash').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);
});

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

  • 下面我们开始使用上面的代码,看看为什么会这么去处理我们的函数。
// 匹配空格
match(/\s+/g, "hello world");
// [ ' ' ]

match(/\s+/g)("hello world");
// [ ' ' ]

// 引出一个 hasSpace 的函数变量,暂存用
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }

// 使用这个 hasSpace 去做一些相关的处理
hasSpaces("hello world");
// [ ' ' ]

hasSpaces("spaceless");
// null

// 现在我们知道了它的返回值,我们可以通过其他函数做进一步的处理。比如筛选出一个有空格的数组值
filter(hasSpaces, ["tori_spelling", "tori amos"]);
// ["tori amos"]

// 保存这个 findSpace 的函数变量
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'
我们为甚要这么多繁琐的步骤,而不是一步到位?

这里表明的是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。分解的使用的函数,让每个函数更具有一定的独立性,使用导出的时候,做到纯净无污染的传递。

扩展我们的 curry

curry 的用处非常广泛,就像在 hasSpacesfindSpacescensored 看到的那样,只需传给函数一些参数,就能得到一个新函数。

  • 下面我们用map包裹一下我们的函数
var getChildren = function(x) {
  return x.childNodes;
};

var allTheChildren = map(getChildren);

只传给函数一部分参数这样的操作通常被称为局部调用(partial application),因为只需要内联调用能够大量减少样板文件代码(boilerplate code)。

当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。

练习一下

  • 这里引用了 ramda,如果没有的话可以手动引入安装一下和引用。
    • npm install ramda
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);
};


// use curry 1:
// ============
// 包裹数组的 `slice` 函数使之成为 curry 函数
// //[1,2,3].slice(0, 2)
var slice = undefined;


// use curry 2:
// ============
// 借助 `slice` 定义一个 `take` curry 函数,该函数调用后可以取出字符串的前 n 个字符。
var take = undefined;

这是上面的答案 Q&A,先别急着看答案,让我们先思考一下

总结

通过简单地传递几个参数,就能动态创建实用的新函数;而且还能带来一个额外好处,那就是保留了数学的函数定义,尽管参数不止一个。

下篇链接 了解 JavaScript 函数式编程 - 代码组合的优势

参考