>  기사  >  웹 프론트엔드  >  JavaScript_javascript 기술의 함수 Currying에 대한 심층 분석

JavaScript_javascript 기술의 함수 Currying에 대한 심층 분석

WBOY
WBOY원래의
2016-05-16 15:10:051654검색

소개
먼저 작은 질문부터 살펴보겠습니다.
누군가가 그룹에 질문을 게시했습니다.
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 
    &#63; 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性能的备注

一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?

通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
  • 在大多是web应用中,“瓶颈”会发生在操控DOM上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.