ここが変だよ CoffeeScript シリーズ
scalar = (x, y) ->
Math.sqrt(x * x + y * y)
この時の scalar.length
は 2
になるのはまぁ当然な事のように思えるはずです。
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.length
は 2
でしょうか?それとも 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.length
は 2
となり、「その関数に必要な最低限の引数の数」は正しく宣言することができます。*なお、 .coffeee の中で Array.prototype.slice
のエイリアスである __slice
を直接記述すると文法エラーになります
でも、ここまでするくらいならそもそも Coffee を選択しない方が賢いって気にさせる一つのトピックでした