CoffeeScriptとスコープ

※本記事は、旧 Tech Talk Blog 内の 「CoffeeScriptとスコープ (http://www.sixapart.jp/techtalk/2012/01/coffeescript.html) はてなブックマーク - CoffeeScriptとスコープ」 で公開されていた記事を移転しました。

こんにちは。MTエンジニアチームの澤田です。

PSVitaが発売されましたね。うっかり発売日にゲームショップに足を運んだら、うっかり在庫があったのでうっかり買ってしまいました。今は「かまいたちの夜」シリーズの最新作「真かまいたちの夜」に夢中です。

「かまいたちの夜」シリーズはサウンドノベルと呼ばれるゲームの代表的な作品です。サウンドノベルは基本的には画面で読む小説といった趣なのですが、時々主人公の行動を決める選択肢が登場し、その選択によって物語の展開や結末が大きく変わってしまうのが特徴です。正しい選択肢を選んで主人公が幸せになるような結末を迎えることがゲームの目的となります。

「かまいたちの夜」シリーズは特に物語展開の幅広さと緻密なフラグ管理が特徴で、物語の序盤の一見どちらでも良いような選択が、最後の最後になって大きく展開を左右する場合があります。完全クリアを目指す場合、ずっと前に選んだ関係なさそうな選択肢まで含めて検討し、正しい選択を選び直すという、恐ろしく根気のいる作業が必要となります。まあそれが楽しいんですけどね。

「真かまいたちの夜」を買う前は、CoffeeScriptに夢中でした。先日社内の勉強会で、JavaScript The Good Partsを見習ってCoffeeScriptの好きなところ嫌いなところをあげていく、という内容のミニトークをしたのですが、その際に一番問題と思われたのが、変数の宣言とスコープでした。

CoffeeScriptはJavaScriptにトランスコンパイルされる言語です。JavaScriptにはレキシカル変数を宣言するvarキーワードがありますが、CoffeeScriptはこれを隠蔽しています。変数の宣言文と代入文に違いは無く、変数が最初に左辺値として評価される箇所が自動的に宣言とみなされます。

CoffeeScript JavaScript
foo = 42
var foo;
foo = 42

この自動宣言は、関数スコープを考慮しません。関数の外側で既にその変数名が使用されていたら、外側の変数を参照したものと扱われます。 

CoffeeScript JavaScript
foo = 42
( -> foo = 43 )()
console.log foo  # prints '43'!
var foo;
foo = 42;
(function() {
return foo = 43;
})();
console.log(foo);

また、明示的にレキシカルスコープを宣言する構文はありません。var宣言はエラーになります。CoffeeScriptとして正しい書き方をする限り、外側のスコープで利用されている変数名を、より内側のスコープで別の目的に再利用する方法はありません。(いくつか抜け道はあります。)

これらのルールは、すこし問題があるように感じます。大きなファイルの最初の方で宣言した変数と同じ名前を、レキシカル変数のつもりで使ってしまった場合や、逆に外側のスコープの変数を参照するつもりでtypoしてしまった場合、きづきにくいバグを生む可能性があります。

Perlをメインの言語として使っているかたは、use strict;を書き忘れたような落ち着かない気持ちになるのではないでしょうか。use strict;の書き忘れはエンジニア生命に関わりますからね。

さらに--joinオプションを使った連結コンパイルでは、問題はより深刻になります。--joinオプションは複数のCoffeeScriptファイルを同じスコープ内でコンパイルする(複数のCoffeeScriptファイルを単純に連結したものがコンパイルされる)からです。

#some.coffee
somefunc = ->
foo = 42 # ローカル変数
#other.coffee
otherfunc = ->
foo = 43 # これもローカル変数

これを--joinオプション付きでコンパイルして使用。

$ coffee -c --join joined.js some.coffee other.coffee
$ cat ./joined.js
(function() {
var otherfunc, somefunc;
somefunc = function() {
var foo;
return foo = 42;
};
otherfunc = function() {
var foo;
return foo = 43;
};
}).call(this);

そして、ずっと後になって、some.coffeeファイルのfoo変数を外に出したくなった。

#some.coffee
foo = 42
somefunc = ->
dosomething foo
#other.coffee
otherfunc = ->
foo = 43 # いつの間にかグローバル変数に!!
$ cat ./joined.js 
(function() {
var foo, otherfunc, somefunc;
foo = 42;
somefunc = function() {
return dosomething(foo);
};
otherfunc = function() {
return foo = 43;
};
}).call(this);

some.coffeeファイルに対して修正を行った事により、別ファイルのotherfunc関数に、グローバル変数に対して意図しない副作用が発生するという問題が発生してしまいました。このようなバグは、そもそも見つけ出す事が難しく、運良く問題に気づいたとしても、ずっと前に宣言した関係なさそうな変数名まで含めて検討し、正しい変数名をつけ直すという、恐ろしく根気のいる作業が必要となります。これはあまり楽しそうではありません。

このような変数宣言とスコープの振る舞いに関しては、当然CoffeScriptのBTSでも議論されています。以下のスレッドでは、Pythonのように変数のスコープはもっとも狭いものとして扱い、globalやouterのような宣言、あるいは特殊な代入演算子(:=)でスコープを変更するようなモデルが提案されています。

そして、CoffeeScriptの作者jashkenasはそれらの提案に対して強く反発しています。上記スレッドからjashkenasの発言をいくつか拾ってみます。邦訳はかなり超訳です。

(前略)

Making assignment and declaration two different "things" is a huge mistake. It leads to the unexpected global problem in JavaScript, makes your code more verbose, is a huge source of confusion for beginners who don't understand well what the difference is, and is completely unnecessary in a language. As an existence proof, Ruby gets along just fine without it.

変数の宣言と代入を別々の"事柄"に分けて考えるのは、大間違いだと思う。それはJavaScriptでは、予期せぬグローバルの問題を導き、みんなのコードを冗長にする。冗長さは違いのよくわかってない初心者にとっては混乱のもとだし、言語にとっても完全に不必要なものだ。その証拠として、Rubyはそれ無しでうまくやっている。

However, if you're not used to having a language without declarations, it seems scary, for the reasons outlined above: "what if someone uses my variable at the top of the file?". In reality, it's not a problem. Only the local variables in the current file can possibly be in scope, and well-factored code has very few variables in the top-level scope -- and they're all things like namespaces and class names, nothing that risks a clash.

とはいえ、今までの議論を見る限り、これまで変数宣言の無い言語を使った事の無い人にとっては、恐ろしいものに感じるようだね。"どこかの誰かが、僕の変数をファイルの先頭で勝手に使ったらいったいどうしたらいいんだい?"。実際のところ、これは問題にはならない。現在のファイルにローカルな変数だけがスコープにふくまれるわけだし、"well-factored"なコードは、トップレベルのスコープではほんの少しの変数しか使わない -- そしてそれらの変数はすべてネームスペースかクラス名だ。衝突の危険はない。

And if they do clash, shadowing the variable is the wrong answer. It completely prevents you from making use of the original value for the remainder of the current scope. Shadowing doesn't fit well in languages with closures-by-default ... if you've closed over that variable, then you should always be able to refer to it.

もし衝突が起こるとしても、変数の隠蔽(shadowing)は間違った解決法だ。スコープ内のその他のすべての変数に関して、オリジナルの値の利用を完全に塞いでしまう。隠蔽は、クロージャが標準(closures-by-default)な言語にはうまくマッチしない。その変数を閉じ込めているならば、常に参照出来るべきだ。

The real solution to this is to keep your top-level scopes clean, and be aware of what's in your lexical scope. If you're creating a variable that's actually a different thing, you should give it a different name.

本当の解決方法は、トップレベルを奇麗な状態に保つ事、そして、何がレキシカルスコープにあるのかを意識する事だ。実際に異なる物事に対して変数を作成するなら、それには異なる名前を付けるべきだろう。

(後略)

そして、それでも議論が収束しないと、次のような事を述べています。

(前略)

I'd like to persuade y'all that strict lexical scope is a defining feature of CoffeeScript -- a massive improvement over the manual var-tagging of variables, and similar in spirit to the notion of structured programming. ... Think of it as "structured variable naming".

僕はみんなに、"strict lexical scope"はCoffeeScriptをCoffeeScript足らしめているものだってことを納得してほしいんだ -- 変数への手動のvarタギングを乗り越える大きな前進であり、精神的には構造化プログラミングの概念とよく似たものだ。 ..."構造化変数命名法"と考えてほしい。

We all know that dynamic scope is bad, compared to lexical scope, because it makes it difficult to reason about the value of your variables. With dynamic scope, you can't determine the value of a variable by reading the surrounding source code, because the value depends entirely on the environment at the time the function is called. If variable shadowing is allowed and encouraged, you can't determine the value of a variable without tracking backwards in the source to the closest var variable, because the exact same identifier for a local variable can have completely different values in adjacent scopes. In all cases, when you want to shadow a variable, you can accomplish the same thing by simply choosing a more appropriate name. It's much easier to reason about your code if a local variable name has a single value within the entire lexical scope, and shadowing is forbidden.

僕らはみんな、ダイナミックスコープはレキシカルスコープに比べて悪いものだと知っている。なぜなら変数の値を推論する事が難しくなるから。ダイナミックスコープでは、変数の値が、関数が呼び出されるときの環境に完全に依存するため、ソースコードの一部を読んで値を決定する事が出来ない。もし変数の隠蔽が許され、奨められている場合、ソースコードの中で一番近いvar変数を探して読み戻る事無しには、変数の値を決定出来ない。なぜなら、ローカル変数が、全く同じ識別子をもつ隣接したスコープの変数と、全く異なる値を持つからだ。変数を隠蔽したくなるときには、いつでも同じ事を、単にもっと適切な名前を選択する事で達成出来る。もしローカル変数の名前がすべてのレキシカルスコープのなかでただ一つの値を持っていて、隠蔽が禁止されているならば、コードはもっと読みやすくなるんだ。

So it's a very deliberate choice for CoffeeScript to kill two birds with one stone -- simplifying the language by removing the "var" concept, and forbidding shadowed variables as the natural consequence.

こいつはCoffeeScriptにとって、まさに一石二鳥な、とても計画的な選択なんだ -- "var"を取り除いて言語をシンプルにし、かつ、自然な成り行きとして変数の隠蔽を禁止出来る。

This brings us to Dmitry's second point: It's still possible to shadow with parameter names, because of the nature of JS functions. I think that Trevor has the right idea here, we should be more strict about shadowing instead of less. It would be great to entertain tickets that either make parameter shadowing a syntax error, or a compile time warning. If we ever go down the road of having a coffee --warn, it should be one of the first rules.

これは、Dmitryの二つ目の論点にもつながっている: JSの関数の本来の動作によって、関数のパラメータを使うと変数の隠蔽は可能なままだ(訳注:なのでスコープのルールが言語内で統一されていない)という問題だ。これは、Trevorのアイデアが正しいと思う。僕らはもっと隠蔽について厳密になるべきだ。パラメータによる隠蔽をシンタックスエラーやコンパイル時の警告にするというのは、おおいに考慮する価値がある。いつかcoffeeコマンドに--warnオプションを追加する日が来たら、こいつは最初のルールにするべきだね。

(中略)

Strict lexical scope isn't going to change in CoffeeScript proper, but I encourage you add outer to your own dialect, or take a look at Coco, which includes two different kinds of variable assignment.

CoffeeScript自体でStrict lexical scopeを変更する予定はないけども、"outer"キーワードをあなたの方言に追加する事や、二種類の代入文を持つCoco(訳注:CoffeeScriptからフォークしたプロジェクト)に注目する事はお勧めするよ。

(後略)

このように、jashkenasはかなり強い姿勢で、現在のCoffeeScriptのスコープの仕組みが意図されたものである事を主張しています。おそらく、当面スコープの振る舞いが修正される事は無いでしょう。

jashkenasの主張は、Perlのstrictプラグマのような、変数宣言を必須とすることでプログラマをケアレスミスから守る、という考え方と真っ向から対立しているように思えます。CoffeeScriptで変数を利用するときに何が起こりうるのかを理解せずに、本番環境で利用すると、落とし穴に落ちそうです。とはいえ、CoffeeScript自体はとても魅力的な言語なので、変数スコープの問題にとらわれて敬遠してしまうのはもったいないと思います。うまく付き合う方法を見つけたいですね。

Six Apart をフォローしませんか?

次の記事へ

パーティーや合コンのお供に、Movable Type

前の記事へ

検索評価やソーシャルメディアでシェアされるリンクのURLを統一するのに必要不可欠なcanonical属性とは