JavaScript
CoffeeScript
FP

((x, y, z…) ->).length is 0

More than 5 years have passed since last update.

ここが変だよ CoffeeScript シリーズ

scalar = (x, y) ->

Math.sqrt(x * x + y * y)

この時の scalar.length2 になるのはまぁ当然な事のように思えるはずです。

f = (a, b = 0) ->

CoffeeScript は上記のイディオムで y == null であった場合にデフォルトに指定した 0 へ代入します。

この場合の f.length はどうでしょうか。結果は 2 です。これも、納得できる結果かと思えます

(これも人によっては 1 であるべきじゃないか、という感覚もある)

それでは、平面以上、n次元上 (n > 1) の座標点を汎用的に受け付ける point 関数をこのように宣言してみます

point = (x, y, cs) ->

vertex = [x, y].concat cs

さて、このときの point.length2 でしょうか?それとも 3 でしょうか。

答えは 0 です。


コンパイル結果

上で記述した宣言はこのような Javascript に展開されます

var point

point = function() {
var x, y, cs, vertex;
x = arguments[0], y = arguments[1], cs = 3 <= arguments.length ? __slice.call(arguments, 2) : [];
return vertex = [x, y].concat cs;
}

これは公式ドキュメントにもちゃんと書いてあることなので、当然そう変換するという前提でコードを書かれているかと思います。

が、 Javascript 初心者に CoffeeScript を薦めるのは愚行だと各場所で主張する理由はこういう部分が所々にあるからです。(他にもあるけど、それは別の記事で紹介します)

f = (x, y) ->     # do something

g = (x, y = 0) -> # do something
h = (x, y, z) -> # do something
k = (x, y, z) -> # do something

console.log f.length # => 2
console.log g.length # => 2
console.log h.length # => 3
console.log k.length # => 0 (!!!)

CoffeeScript のソースコード上でこのような結果は、一見しただけで予想しやすいかとは思えません。


困ることある?


arity が意図していない 0 による弊害

この仕様で困る事があるかというと、たとえば printf のような任意の数の変数を受け取る関数を定義する際に "必須である引数が充分に渡されていない場合にエラーなどをライブラリ使用者に示す" / "部分関数として残りの引数を待つ関数を作る" などの処理が実装できません。(素の Javascript で素直に書けばそのようなユーティリティはトリックなしで提供できる)


args… をクロージャ内に配列で不必要に確保することによる弊害

もう一つは、for while などで繰り返し関数を呼び出す際に、普通に Javascript に書くよりもパフォーマンスが低下します。その原因は使う必要が無くとも、呼び出す度に可変長引数の部分を var opts = [] としてメモリを確保するので、 GC Event を頻繁に呼ばれる可能性が出てくることによります。書き方次第では GC されにくい故にメモリリークの原因になりえます

// javascript でこう書き直したら速度とメモリ肥大が収まった、というケース

var _slice = Array.prototype.slice
var f = function(x, y) {
if (2 < arguments.length) {
g.apply(null, _slice.call(arguments));
} else {
g(x, y);
}
};


すごく気づきにくい

あまりそのようなトラブルが起きる事はないと思いますが、これを原因とするバグに直面した際に、原因が (x, y, z…) と書かれていた、というのに気づくのは困難かと思われます。 SourceMap に対応したから Chrome でもデバッグしやすくなった、と喜んでも、ここには気付けないかと。


対策


(a, b, c, others…) のイディオム使わない

自前で Array.prototype.slice のエイリアスを用意して、 CoffeeScript のイディオムを経由しないように工夫する

_slice = Array::slice

f = (x, y) ->
# 必要性が出てくるまで cs への代入を記述しないでおく
cs = if 2 < arguments.length then _slice.call arguments, 2 else []
# do something

この時の f.length2 となり、「その関数に必要な最低限の引数の数」は正しく宣言することができます。なお、 *.coffeee の中で Array.prototype.slice のエイリアスである __slice を直接記述すると文法エラーになります

でも、ここまでするくらいならそもそも Coffee を選択しない方が賢いって気にさせる一つのトピックでした