JS函数柯里化

521 阅读5分钟

定义

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

例如,一些分析技术只能用于具有单一参数的函数。现实中的函数往往有更多的参数。为单一参数情况提供解决方案已经足够了,因为可以将具有多个参数的函数转换为一个单参数的函数链。这种转变是现在被称为“柯里化”的过程。

实现

我们先写一个实现加法的函数 add:

function add (x, y) {
  return (x + y)
}
add (1, 3);//4

根据柯里化的条件,写一个 curriedAdd 的函数。

function curriedAdd (x) {
  return function(y) {
    return x + y
  }
}
let addOne=curriedAdd(1);
addOne(3);//4

当然以上实现是有一些问题的:它并不通用,我们想实现一个通用的方法而不是每次都需要针对某个问题实现 currying 化。

此时我们需要用到 JavaScript 中的作用域来保存上一次传进来的参数。 对 curriedAdd 进行抽象,可能会得到如下函数 currying :

function currying (fn, ...args1) {
    return function (...args2) {
        return fn(...args1, ...args2)
    }
}
var increment = currying(add, 1)
increment(2) === 3
// true

在此实现中,currying 函数的返回值其实是一个接收剩余参数并且返回计算值的函数,它的返回值并没有自动被 Currying化。

具体的说,如果需要 currying 化的函数的参数变成3个,按照这种写法

function add3 (x, y, z) {
  return (x + y + z)
}

var increment = currying(add3, 1)
increment(2, 3) // 6
increment(2)(3) // increment(...) is not a function

因为 increment(2) 没有被 currying 化,所以会报错。而实际上我们希望increment(2)(3)结果等同于 increment(2, 3),这样需要利用函数递归改进方法。

function trueCurrying(fn, ...args) {
    if (args.length >= fn.length) {
        return fn(...args)
    }
    return function (...args2) {
        return trueCurrying(fn, ...args, ...args2)
    }
}

以上函数很简短,但是这个方法已经可以将js函数 Currying 化。然而 Currying 的概念和如何实现都不是最重要的,重点是:它能够解决编码和开发当中怎样的问题。

使用场景

先举个例子,有这么一大块数据

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"}
        // , ...
    ]
};

现在我们要处理它,找到用户 Scott 的所有未完成任务,并按到期日期升序排列。原文给了一段这样的方法。

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;
        });
};
getIncompleteTaskSummaries('Scott').then(r => console.log(r));

这段代码很长,但是很简单,这不就是我们经常做的事情,当然我觉得这段代码可以稍微改进一下,毕竟我们有了 ES6。

var getIncompleteTaskSummaries = function (membername) {
    return Promise.resolve(data)
        .then(data => {
            return data.tasks;
        })
        .then(tasks => {
            return tasks.filter((task) => {
                return task.username === membername
            });
        })
        .then(tasks => {
            return tasks.filter((task) => {
                return !task.complete
            });
        })
        .then(tasks => {
            return tasks.map((task) => {
                return {
                    id: task.id,
                    dueDate: task.dueDate,
                    title: task.title,
                    priority: task.priority
                }
            });
        })
        .then(tasks => {
            return tasks.sort((first, second) => {
                return first.dueDate < second.dueDate ? -1
                    : first.dueDate > second.dueDate ? 1 : 0;
            });
        });
};
getIncompleteTaskSummaries('Scott').then(r => console.log(r));

好了,稍微好了一些,这基本上就是我们正常写的代码了。我们可以看到,我们每一次都是把我们要处理的数据加工一下,得到的结果送到下一个函数体内加工。如果按照函数式编程的思想,这些中间量其实都是不需要的。

那么现在我们要用一个叫 ramda 的js组件

const R = require("ramda");
var getIncompleteTaskSummaries = function(membername) {
    return Promise.resolve(data)
        .then(R.prop('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.prop('dueDate')));
};

getIncompleteTaskSummaries('Scott').then(r => console.log(r));

执行这段代码,结果和之前的一样,但是简洁程度不用多说了。

但是看完这个例子,仔细推敲其实就能明白柯里化到底做了什么。如果还是不清楚,仔细看一下 ramda 的源码,大多都是柯里化的函数。在 ramda 中随便挑一个命名为 prop 的函数感受一下:

R.prop('dueDate') 

// 第一个参数p为'dueDate'
var prop = /*#__PURE__*/_curry2(function prop(p, obj) {
  return path([p], obj);
});

function _curry2(fn) {
  return function f2(a, b) {
    switch (arguments.length) {
      case 0:
        return f2;
      case 1:
        return _isPlaceholder(a) ? f2 : _curry1(function (_b) {
          return fn(a, _b);
        });
      default:
        return _isPlaceholder(a) && _isPlaceholder(b) ? f2 : _isPlaceholder(a) ? _curry1(function (_a) {
          return fn(_a, b);
        }) : _isPlaceholder(b) ? _curry1(function (_b) {
          return fn(a, _b);
        }) : fn(a, b);
    }
  };
}

function _isPlaceholder(a) {
       return a != null && typeof a === 'object' && a['@@functional/placeholder'] === true;
}

function _curry1(fn) {
  return function f1(a) {
    if (arguments.length === 0 || _isPlaceholder(a)) {
      return f1;
    } else {
      return fn.apply(this, arguments);
    }
  };
}

//真正的处理业务的方法
var path = /*#__PURE__*/_curry2(function path(paths, obj) {
  var val = obj;
  var idx = 0;
  while (idx < paths.length) {
    if (val == null) {
      return;
    }
    val = val[paths[idx]];
    idx += 1;
  }
  return val;
});

ramda 代码都很简单,我觉得更像教科书,大家可以自己研究一下。

柯里化不是讳莫如深的东西,光看概念是不好理解,实际运用起来就是这么简单,原来还可以这么封装函数。

参考

JavaScript高级程序设计

Pointfree 编程风格指南

Favoring Curry

大佬,JavaScript 柯里化,了解一下?