JavaScriptにおいて、関数の中でarguments.length
は良く目にする/使うと思うのですが、関数fn自体のfn.length
を利用することはまれではないでしょうか。
(これ以降fnは全て、定義された関数を表すことにします)
async.angelFall
を社内に布教しようと思ったのですが、そういえば前のエントリで言及しかけていたので、どうせならfn.length
の話題とまとめてブログに書こうと思った次第。
fn.lengthとは
Function.length - JavaScript | MDN
arguments.length
が関数が実際に呼ばれた時の引数の個数なのに対して、fn.length
はその関数fnが定義されたときの仮引数の個数を表します。
var fn = function(a, b, c) {}; fn.length // 3
Function#bindで第2引数以降を渡すといわゆるカリー化ができますが、きちんとこのlengthの値も変わったりします。頭良いですね。
var fn = function(a, b, c) {}; // 引数aに1を部分適用した新しい関数を作る var fn_curried = fn.bind(null, 1); // b, cの2つの引数をとる関数になっている fn_curried.length // 2
使用例
コイツの使用例をいくつか紹介したいと思います。
lodash
まずは大御所lodashにおける使用例です。関連するissueはコチラ。(読まなくても下で全部説明しますが、読んで分かったら多分このエントリにそれ以上のことは無いです。)
https://github.com/lodash/lodash/issues/944
https://github.com/lodash/lodash/issues/997
さて、この問題の背景としていくつか説明する必要があります。
iterateeに渡る引数
まず、lodashのeachやmapなど(多数)のメソッドに第2引数として渡す、関数(lodashではiterateeと呼ばれています)は、次のように引数を3つ取ることが出来ます。
var arr = ['a', 'b', 'c']; _.map(arr, function(value, index, array) { // value: 配列の値。 順に 'a'/'b'/'c' が入る // index: 配列のindex。 順に 0/1/2が入る // array: 配列全体。 ここでは毎度 ['a', 'b', 'c'] が入ってくる });
これはJavaScriptのArray#forEach
などと同じ仕様です。
lodashのメソッドチェーン
次に、lodashにはメソッドチェーンをする書き方があります。例としてはこんな感じ。(少し煮え切らない例ですが)
var arr = [1, 2, 3, 4, 5]; var result = _(arr) // _.chain(arr)でも同じ .map(function(value, index, array) { return value * value; // 2乗 }) .filter(function(value, index, array) { return value % 2 === 1; // 奇数 }) .take(2) // 配列の最初から2つを取得 .value(); // 実際の結果を取得 result // [1, 9]
lodash v2までは、単に順番に _.map
, _.filter
, _.take
を適用していくだけでした。すなわち、以下と基本同じです。
var arr = [1, 2, 3, 4, 5]; var mapped = _.map(arr, function(value, index, array) { return value * value; }); var filtered = _.filter(mapped, function(value, index, array) { return value % 2 === 1; }); var result = _.take(filtered, 2); result // [1, 9]
lodashの遅延評価
しかしlodash v3で遅延評価(lazy evaluation)が登場したことで少し話は変わります。
上記の例でいうと、配列をまずmapして[1, 4, 9, 16, 25]
を得て、その後filterして[1, 9, 25]
を得たうえで、takeで頭2つを取って[1, 9]
を得ています。しかしながら、最初から「頭2つを取る」とわかっていれば、全て計算しなくとも、map・filterを手前からしていって[1, 9]
まで得られた時点で打ち切っても良いことになります。
メソッドチェーンの方法で書けば、文として切れ目がないので、必ずしも[1, 4, 9, 16, 25]
とか[1, 9, 25]
という結果を得る必要はないわけです。そこで、メソッドチェーンの書き方をされた時に、可能であれば遅延評価をするような実装がなされました。
一方、後者の例ではmappedやfilteredという変数に一旦代入しているため、当然結果を計算する必要があります。(残念ながら、JSのエンジンレベルの遅延評価はありませんから・・)。これがlodash v3で実装された遅延評価の優位性です。
ちなみに、遅延評価時の実行の流れは大体こんな感じです。map=>filter=>takeの時点では、内部的に状態を持つだけで、計算はしません。そして、value()が実行されたタイミングで、以下のような手順で計算されます。
arr[0]
である1
をmap(2乗)する。1
。- 結果の
1
にfilter(奇数)の関数をかける。true
。 true
なので、結果の配列にpushする。[1]
。※take 2なのでまだおわらない。arr[1]
である2
をmapする。4
。- 結果の
4
にfilterの関数をかける。false
。 false
なのでなにもしない。arr[2]
である3
をmapする。9
。- 結果の
9
にfilterの関数をかける。true
。 true
なので、結果の配列にpushする。[1, 9]
。 take 2なのでここで終了。[1, 9]
を返す。
これにより、頭3つ分しか計算せずにすんでいます。
普通は1回ずつ配列を生成して次に進むのに対して、
遅延評価が効く場合は、配列の要素1つずつがchainを通り抜けていくイメージですね。
さて、これで背景の説明は終わりです。すでにお気づきでしょうか?
遅延評価で生じた、iteratee第2・第3引数の問題
map=>filter=>takeの真ん中、 filter
に渡したiterateeの第3引数arrayに注目してみます。
本来ならばここには、直前でmapした結果である [1, 4, 9, 16, 25]
が入るのが正しいはずです。
しかし、上の遅延評価の動作を見れば分かるように、mapした全体の結果 [1, 4, 9, 16, 25]
は作っていません。ということは、第3引数のarrayには正しい値が渡ってこないことになります。(作ってないものは渡しようがない!)
普段はこの第3引数などまず使いませんが、_.uniqを遅延評価に対応させるPRを送ろうと実装をしていてたまたまこのことに気付き、issueを立てました。(拙い英語ですが)
その後、もう片方のissueで、第2引数のindexも正しさが保証されないことがわかりました(より重要な問題)。
fn.lengthを用いて修正される
これに対して、jdalton 氏が機転の利いた対応を行いました。
上の例では、遅延評価してしまうと、そもそもiterateeに第2引数のindexや第3引数のarrayを正しく渡すことができないのです。しかし、ほとんどの用途では第1引数のvalueしかとらず、そのときは問題ない。第2引数以降を渡さなければいけないケースだけ、遅延評価をとりやめれば良いのです。すなわち、iterateeの仮引数の数を見れば良い。それがこのコミットです。
iteratee.length > 1
のとき、isLazy
がfalse
になっているのがわかります。
これを見た当時、fn.lengthはこうやって使えるのかと、心の底から感心しました。自分がissueを立てた時点ではこの方法は全く思いつかなかったです。
以上、lodashにおけるfn.lengthの利用例の紹介でした。(長かった。)
fn.lengthを利用する潜在的な問題
次の2つのコードの違いはなんでしょうか。
var fn = function(a) { return a; }
var fn = function() { return arguments[0]; }
答えはもちろん「fn.lengthの値が違う」です。(他にもfn.toString()の結果が違うなどありますが)
fn.lengthはあくまで仮引数の個数なのですが、arguments
を使えば、仮引数として宣言されていなくてもその引数にアクセスすることは可能です。
lodashの例でいけば、仮引数を宣言せずにiteratee内で第2・第3引数にarguments[1]
arguments[2]
などとアクセスした場合には結局、期待された値が入らないことがあります。
この問題はfn.lengthを使うと常に潜在してしまいます。
最後に
普通にプログラミングしてても絶対使わないようなものも、ライブラリ書くときにはたまに出くわすよね、という良い例だと思います。実際のプロジェクトでも、基盤的な機能の整備をするときには色々知っておくと便利そうです。
あと、lodash は結構面白いなと思います。
async.angelFallの話を書こうとしてたはずなんですがね。次で書く。