JavaScript で引数束縛2005年12月13日 18時14分

引数束縛 (カリー化) の話。まずは「JavaScriptでカリー化」(檜山正幸のキマイラ飼育記)。タイトルを見てどこかで聞いたような話だなと思ったら「関数の変形」(Effecttive JavaScript - Dynamic Scripting) だった。だが、前者は文字列に戻してから評価というのが力技っぽくて個人的に好きでないし、後者は汎用的過ぎていささかわかりにくい。そこで今回は先頭の引数から束縛していくというのに的を絞ってみたいと思う。

まず第 1 引数のみを束縛する場合。Function#apply を使えば引数を配列として渡せるので、束縛された値と後から渡された引数とを連結してやればいいのではないか。

function curry(func)
{
  return function (first) {
    return function () {
      var args = Array.prototype.concat.apply([first], arguments);
      return func.apply(this, args);
    };
  };
}

function sum(x, y) { return x + y; }
function mean3(a, b, c) { return (a + b + c) / 3; }

alert( curry(sum)(10)(15) );               // 25
alert( curry(mean3)(10)(20, 30) );         // 20
alert( curry(curry1(sum))(10)()(20) );     // 30
alert( curry(curry1(mean3)(10))(20)(30) ); // 20

それなりにいい感じである。檜山さんが動かないといっていた最後のパターンもちゃんと計算できているようだ。では調子に乗って束縛する引数の数を可変にしてみよう。

function curry(func)
{
  return function () {
    var length = arguments.length;
    var heads = new Array(length);
    for (var i = 0; i < length; i++)
      heads[i] = arguments[i];

    return function () {
      var args = Array.prototype.concat.apply(heads, arguments);
      return func.apply(this, args);
    };
  };
}

alert( curry(sum)(10, 20)() );    // 30
alert( curry(mean3)(5, 15)(10) ); // 10

これでうまくいくと思ったら落とし穴が。引数に配列があるとそれを展開してしまうのだ。

alert( sum(1, [2, 4]) );        // 12,4
alert( curry(sum)(1)([2, 4]) ); // 3
// sum は二つしか引数をとらないので最後の 4 は無視されている。

そこで後から受け取る引数も配列に変換することに。

function curry(func)
{
  return function () {
    var length = arguments.length;
    var heads = new Array(length);
    for (var i = 0; i < length; i++)
      heads[i] = arguments[i];

    return function () {
      var length = arguments.length;
      var tails = new Array(length);
      for (var i = 0; i < length; i++)
        tails[i] = arguments[i];

      var args = heads.concat(tails);
      return func.apply(this, args);
    };
  };
}

alert( sum(1, [2, 4]) );        // 12,4
alert( curry(sum)(1)([2, 4]) ); // 12,4

さすがにコードが長くなってきたので似たような処理をまとめる。prototype.js にも同名のメソッドがあるが Array.from を定義。

Array.from = function (array) {
  var length = array.length;
  var result = new Array(length);
  for (var i = 0; i < length; i++)
    result[i] = array[i];
  return result;
};

function curry(func)
{
  return function () {
    var heads = Array.from(arguments);
    return function () {
      var tails = Array.from(arguments);
      var args = heads.concat(tails);
      return func.apply(this, args);
    };
  };
}

これでかなりすっきりした。カリー化された関数に対して、複数回の部分適用ができるようにしたものを「JavaScript でカリー化、再び」に書いた。ついでに Function.prototype に結びつければ個人的にはさらにすっきり。以下で定義してある curry メソッドがしていることは、その名に反してカリー化ではなく部分適用である。

Function.prototype.curry = function () {
  var heads = Array.from(arguments);
  var self = this;
  return function () {
    return self.apply(this, heads.concat(Array.from(arguments)));
  };
};

alert( sum.curry(10)(15) );             // 25
alert( mean3.curry(10)(20, 30) );       // 20
alert( sum.curry(10).curry()(20) );     // 30
alert( mean3.curry(10).curry(20)(30) ); // 20
alert( sum.curry(10, 20)() );           // 30

ななしさんからのヒントにより Array.from を使わなくてもよくなった。

Function.prototype.curry = function () {
  var args = arguments;
  var self = this;
  return function () {
    Array.prototype.push.apply(args, arguments);
    return self.apply(this, args);
  };
};

ななしさんnak2k さんのコメントにより Array.from を使わなくてもよくなった。

Function.prototype.curry = function () {
  var args = arguments;
  var self = this;
  return function () {
    Array.prototype.unshift.apply(arguments, args);
    return self.apply(this, arguments);
  };
};

コメント

_ ななし ― 2005年12月14日 16時18分

はじめまして。ななしと名乗ります。
// JS 歴は、NN2 の終わり頃からなので、8年強です。
// ECMA262-1,2,3 はそれなりに追ってきたつもりですが、
// JavaScript-C 実装のごく一部しか読解していない程度のヘタレです。

> 引数に配列があるとそれを展開してしまうのだ。
さすがです。多くの人が手を抜いたり、そもそも、気付いていませんよね。
// Function#apply で展開された引数リストの内、
// 配列は、Array#concat で更に展開されてしまう。
// 逆に、これを利用すれば、Ruby の Array#flatten あたりは
// 簡潔に書けて真似出来ますよね。

そこで、Array#splice を使ってみました。
また、"self" が "this" を使っている場合に備えて、
"thisArg" と言う名前の引数を持てるようにしてみましたので、
function の入れ子は元に戻っています。

function Function_prototype_curry(thisArg) {
var f = this;
return function () {
var as = arguments;
Array.prototype.splice.call(as, 0, 0, 0, 0);
return function () {
Array.prototype.splice.apply(arguments, as);
return f.apply(thisArg, arguments);
};
};
}

_ nanto_vi ― 2005年12月15日 14時05分

なるほど。Arguments オブジェクトをどうやって Array にするか / Array として扱うかには悩んだのですがその手がありましたか。ありがとうございます。

ちなみに Array#flatten は前に試したことがあります。
「多元配列を一元配列に変換」
http://nanto.asablo.jp/blog/2005/10/08/101406

_ nak2k ― 2005年12月19日 16時23分

はじめまして。nak2kと申します。
「ななしさんからのヒントにより~」の箇所の関数なのですが、カリー化functionを生成したコンテキストにクロージングされた変数 args を Array#push で変更してしまってるために、以下のような形で意図しない動作になりませんか?

function test(a,b,c,d) { return a+b+c+d; }
var f = test.curry(1,2);
f(3,4); // --> 10, args == [1,2,3,4]
f(2,4); // --> 10, args == [1,2,3,4,2,4]
f(1,4); // --> 10, args == [1,2,3,4,2,4,1,4]

ちなみに私の場合、ArgumentsオブジェクトのArray化は、 Array.apply(null, arguments) で行うことが多いのですが、今回のケースではななしさんのArray#splice方式のほうが性能よさそうですね。(splice.call 呼び出しの引数に 0 が4つある意味が一瞬分かりにくかったですが^^;)

_ nanto_vi ― 2005年12月19日 22時43分

あー、確かにそうですね。全然気づいて (テストして) いませんでした。ご指摘ありがとうございます。早速修正しました。
# ななしさんの splice に 0 が 4 つは私も最初「?」でした。

_ 仕様書係 ― 2008年12月11日 08時56分

大変勉強になりました。
ありがとうございます。

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2005/12/13/176033/tb

_ 檜山正幸のキマイラ飼育記 - 2005年12月14日 12時08分

来たよ、来ましたよー。 僕がJavaScriptやブラウザの話を書くと、誤解・誤認の指摘や役に立つ情報の提供が、コメントやトラックバックで来ます。 (http://d.hatena.ne.jp/m-hiyama/20051210/1134187007)このジンクス(?)は正しかった。 うーん、テキストつぎはぎは、やっぱダサイな -- いいんか? これで (http://d.hatena.ne.jp/m-hiyama/20051213/1134446855)と、カリー化するのに、関数ソーステキス ...

_ 俺の基地 (PukiWiki/TrackBack 0.3) - 2008年03月03日 11時56分

関連ページ Javascript/あまり頭を使わない継承 Javascript/イベントのバブリングという現象 Javascript/インライン要素の幅を取得する Javascript/オブジェクトに指定したプロパティが存在するか確認する Javascript/カンマ演算子 Javascript/クロスサイトスク...