소개
먼저 작은 질문부터 살펴보겠습니다.
누군가가 그룹에 질문을 게시했습니다.
var s = sum(1)(2)(3) ....... 최종적으로 경고는 6으로 나옵니다
var s = sum(1)(2)(3)(4) ....... 최종적으로 경고는 10으로 나옵니다
합계를 구현하는 방법을 물어보십시오.
제목을 처음 봤을 때 첫 번째 반응은 sum이 함수를 반환한다는 것이었습니다. 그러나 최종적으로는 구현되지 않았습니다. 비슷한 원리를 마음속으로 보았지만 명확하게 기억할 수는 없었습니다.
나중에 동료가 이걸 커링이라고 하더군요
구현 방법이 더 영리합니다.
function sum(x){ var y = function(x){ return sum(x+y) } y.toString = y.valueOf = function(){ return x; } return y; }
카레링을 자세히 살펴볼까요~
커링이란 무엇인가요?
커링은 여러 매개변수를 허용하는 함수를 단일 매개변수(참고: 원래 함수의 첫 번째 매개변수)를 허용하는 함수로 변환하는 변환 프로세스입니다. 나머지 매개변수를 처리하고 결과를 반환합니다.
그렇게 말하면 카레는 꽤 간단하게 들리는 것 같아요. JavaScript에서는 어떻게 구현되나요?
3개의 매개변수를 받는 함수를 작성한다고 가정해 보겠습니다.
var sendMsg = function (from, to, msg) { alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n")); };
이제 전통적인 JavaScript 함수를 카레 함수로 변환하는 카레 함수가 있다고 가정해 보겠습니다.
var sendMsgCurried = curry(sendMsg); // returns function(a,b,c) var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob"); // returns function(c) sendMsgFromJohnToBob("Come join the curry party!"); //=> "Hello Bob, Come join the curry party! Sincerely, - John"
수동 커링
위의 예에서는 신비한 카레 기능이 있다고 가정합니다. 그러한 기능을 구현하고 싶지만 지금은 먼저 그러한 기능이 왜 그렇게 필요한지 살펴보겠습니다.
예를 들어 함수를 수동으로 커링하는 것은 어렵지 않지만 약간 장황합니다.
// uncurried var example1 = function (a, b, c) { // do something with a, b, and c }; // curried var example2 = function(a) { return function (b) { return function (c) { // do something with a, b, and c }; }; };
JavaScript에서는 함수의 모든 매개변수를 지정하지 않아도 함수가 호출됩니다. 이것은 매우 유용한 JavaScript 기능이지만 카레링에 문제를 일으킵니다.
모든 함수는 단 하나의 매개변수를 갖는 함수라는 개념입니다. 여러 매개변수를 갖고 싶다면 서로 중첩된 일련의 함수를 정의해야 합니다. 싫어하다! 한두 번 정도는 괜찮지만, 이렇게 많은 매개변수가 필요한 함수를 정의해야 할 경우 상당히 장황해지고 읽기 어려워집니다. (하지만 걱정하지 마세요. 바로 방법을 알려드릴께요)
Haskell 및 OCaml과 같은 일부 함수형 프로그래밍 언어에는 구문에 함수 커링이 내장되어 있습니다. 예를 들어 이러한 언어에서 모든 함수는 하나의 인수와 단 하나의 인수만 취하는 함수입니다. 이 제한이 이점보다 더 크다고 생각할 수도 있지만 언어의 구문을 보면 이 제한은 거의 눈에 띄지 않습니다.
예를 들어 OCaml에서는 위의 예를 두 가지 방법으로 정의할 수 있습니다.
let example1 = fun a b c -> // (* do something with a, b, c *) let example2 = fun a -> fun b -> fun c -> // (* do something with a, b, c *)
이 두 가지 예가 위의 두 예와 어떻게 유사한지 쉽게 알 수 있습니다.
그러나 차이점은 OCaml에서도 동일한 작업이 수행되는지 여부입니다. OCaml에는 여러 매개변수를 가진 함수가 없습니다. 그러나 한 줄에 여러 매개변수를 선언하는 것은 단일 매개변수 함수를 중첩하는 "간단한 방법"입니다.
마찬가지로, 카레 함수를 호출하는 것은 OCaml에서 다중 매개변수 함수를 호출하는 것과 구문적으로 유사할 것으로 예상됩니다. 우리는 위 함수를 다음과 같이 호출할 것으로 예상합니다:
example1 foo bar baz example2 foo bar baz
JavaScript에서는 상당히 다른 접근 방식을 취합니다.
example1(foo, bar, baz); example2(foo)(bar)(baz);
OCaml과 같은 언어에는 카레링이 내장되어 있습니다. JavaScript에서는 커링(고차 함수)이 가능하지만 구문상 불편합니다. 이것이 바로 우리가 이러한 지루한 작업을 수행하고 코드를 더 단순하게 만들기 위해 카레 함수를 작성하기로 결정한 이유입니다.
카레 도우미 기능 만들기
이론적으로 우리는 일반 JavaScript 함수(여러 매개변수)를 완전히 카레된 함수로 변환하는 편리한 방법을 원합니다.
이 아이디어는 나만의 것이 아니며, wu.js 라이브러리의 .autoCurry() 함수와 같은 다른 사람들이 이를 구현했습니다(비록 여러분이 우려하는 것은 우리 자체 구현입니다).
먼저 간단한 도우미 함수인 .sub_curry를 만들어 보겠습니다.
function sub_curry(fn /*, variable number of args */ ) { var args = [].slice.call(arguments, 1); return function () { return fn.apply(this, args.concat(toArray(arguments))); }; }
잠시 이 기능이 어떤 역할을 하는지 살펴보겠습니다. 아주 간단합니다. sub_curry는 함수 fn을 첫 번째 인수로 받아들이고 그 뒤에는 원하는 만큼의 입력 인수가 옵니다. 반환되는 것은 함수입니다. 이 함수는 fn.apply의 실행 결과를 반환합니다. 매개변수 시퀀스는 처음에 함수에 전달된 매개변수와 fn이 호출될 때 전달된 매개변수를 결합합니다.
예 보기:
var fn = function(a, b, c) { return [a, b, c]; }; // these are all equivalent fn("a", "b", "c"); sub_curry(fn, "a")("b", "c"); sub_curry(fn, "a", "b")("c"); sub_curry(fn, "a", "b", "c")(); //=> ["a", "b", "c"]
물론 이것이 우리가 원하는 것은 아니지만 약간 카레가 되는 것 같습니다. 이제 커링 함수인 curry를 정의하겠습니다:
function curry(fn, length) { // capture fn's # of parameters length = length || fn.length; return function () { if (arguments.length < length) { // not all arguments have been specified. Curry once more. var combined = [fn].concat(toArray(arguments)); return length - arguments.length > 0 ? curry(sub_curry.apply(this, combined), length - arguments.length) : sub_curry.call(this, combined ); } else { // all arguments have been specified, actually call function return fn.apply(this, arguments); } }; }
이 함수는 두 개의 매개변수, 즉 함수와 "커리"할 매개변수 수를 허용합니다. 두 번째 매개변수는 선택사항입니다. 생략하면 이 함수가 정의하는 매개변수 수를 알려주기 위해 기본적으로 Function.prototype.length 속성이 사용됩니다.
궁극적으로 다음 동작을 시연할 수 있습니다.
var fn = curry(function(a, b, c) { return [a, b, c]; }); // these are all equivalent fn("a", "b", "c"); fn("a", "b", "c"); fn("a", "b")("c"); fn("a")("b", "c"); fn("a")("b")("c"); //=> ["a", "b", "c"]
我知道你在想什么…
等等…什么?!
难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。
这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。
柯里化和“洞”(“holes”)
尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。
当然这个只能工作在当最左参数就是你想要分步应用的参数!
为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。
这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。
举个例子,我们有这样的一个函数:
var sendAjax = function (url, data, options) { /* ... */ }
也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。
当然了,我们能够相当简单的这样定义函数:
var sendPost = function (url, data) { return sendAjax(url, data, { type: "POST", contentType: "application/json" }); };
或者,使用使用约定的下划线方式,就像下面这样:
var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });
注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?
回过头让我们把curry函数变得智能一点…
首先我们把我们的“占位符”定义成一个全局变量。
var _ = {};
我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。
不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:
function curry (fn, length, args, holes) { length = length || fn.length; args = args || []; holes = holes || []; return function(){ var _args = args.slice(0), _holes = holes.slice(0), argStart = _args.length, holeStart = _holes.length, arg, i; for(i = 0; i < arguments.length; i++) { arg = arguments[i]; if(arg === _ && holeStart) { holeStart--; _holes.push(_holes.shift()); // move hole from beginning to end } else if (arg === _) { _holes.push(argStart + i); // the position of the hole. } else if (holeStart) { holeStart--; _args.splice(_holes.shift(), 0, arg); // insert arg at index of hole } else { _args.push(arg); } } if(_args.length < length) { return curry.call(this, fn, length, _args, _holes); } else { return fn.apply(this, _args); } } }
实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。
展示下我们的新帮手,下面的语句都是等价的:
var f = curry(function(a, b, c) { return [a, b, c]; }); var g = curry(function(a, b, c, d, e) { return [a, b, c, d, e]; }); // all of these are equivalent f("a","b","c"); f("a")("b")("c"); f("a", "b", "c"); f("a", _, "c")("b"); f( _, "b")("a", "c"); //=> ["a", "b", "c"] // all of these are equivalent g(1, 2, 3, 4, 5); g(_, 2, 3, 4, 5)(1); g(1, _, 3)(_, 4)(2)(5); //=> [1, 2, 3, 4, 5]
疯狂吧?!
我为什么要关心?柯里化能够怎么帮助我?
你可能会停在这儿思考…
这看起来挺酷而且…但是这真的能帮助我编写更好的代码?
这里有很多原因关于为什么函数柯里化是有用的。
函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。
为了给一个简单的例子,让我们分别使用Vanilla.js, Underscore.js, and “函数化方式” (极端利用函数化特性)来编写CSV解析器。
Vanilla.js (Imperative)
//+ String -> [String] var processLine = function (line){ var row, columns, j; columns = line.split(","); row = []; for(j = 0; j < columns.length; j++) { row.push(columns[j].trim()); } }; //+ String -> [[String]] var parseCSV = function (csv){ var table, lines, i; lines = csv.split("\n"); table = []; for(i = 0; i < lines.length; i++) { table.push(processLine(lines[i])); } return table; }; Underscore.js //+ String -> [String] var processLine = function (row) { return _.map(row.split(","), function (c) { return c.trim(); }); }; //+ String -> [[String]] var parseCSV = function (csv){ return _.map(csv.split("\n"), processLine); };
函数化方式
//+ String -> [String] var processLine = compose( map(trim) , split(",") ); //+ String -> [[String]] var parseCSV = compose( map(processLine) , split("\n") );
所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。
想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。
关于curry性能的备注
一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?
通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。
有关性能,这里有一些事情必须牢记于心: