クロージャの使い所についてのメモ

自分自身のクロージャの捉え方が正常値の範囲内であるか確認する為、何か書いて晒してみるテスト。

具体的には、JScriptで小ツールを実装している時にクロージャを使ってみた話。WSHWindows Script Host)5.6でJScriptを使っているので、その環境に依存した話を題材としている。

クロージャがどんなものであるか」というのは重要な話だけど、同時に「クロージャを使うと何ができるのか」というのも大切な話で、何ができるか示す時に具体的な例を出す事が大切なのではないかと思う。なぜなら、ソフトウェアを実装する時に役に立たなければ誰もクロージャなんて使わないからだ*1

前フリ

WSHJScriptスクリプトを実行する方法は幾つかある。

cscript foo.js
  • コマンドプロンプト等からwscriptで実行する。wscriptはスクリプトをウインドウベースっぽい感じで実行する。cscriptのような感じで標準入出力を使うことはできない。
wscript foo.js
foo.js

実行方法の違いによってスクリプトの挙動が変わる。例えばWScript.Echoという出力用のメソッドがあるのだけど、cscriptで実行した場合は文字列を標準出力に書き出し、それ以外の場合は文字列をメッセージボックスに表示する。

この挙動の変化は、他人に配布する予定のあるスクリプトを書く時に悩むことになる。例えばデータをWScript.Echoで1件ずつ出力する書き方をしていると、cscript以外で実行されると50件のデータが1件ずつ50回メッセージボックスで表示されるとか、そんな惨事が待ち構えていたりする。

その為、スクリプト側で自分自身がどうやって実行されているかを判断して、エラーで中断するなり一部挙動を変更するなり工夫することになる。

問題はどうやって判断するかなのだけど、決定的な方法はない。ただ自分自身(スクリプト)を実行しているスクリプトホスト名を取得できるので、それを使って判断する*2

// script_host1.js
(function() {
	var fsys = WScript.CreateObject("Scripting.FileSystemObject");
	var name = fsys.GetBaseName(WScript.FullName);

	WScript.Echo(name);
 })();

このスクリプトを実行すると次のようになる*3

実行方法 出力
コマンドプロンプトから「cscript script_host1.js」と実行 cscript
コマンドプロンプトから「wscript script_host1.js」と実行 wscript
コマンドプロンプトから「script_host1.js」と実行 WScript
エクスプローラからダブルクリック WScript

本題

スクリプトホスト名を取得する方法が分かったので、関数化してみる。

// script_host2.js
(function() {
	/// このスクリプトを実行しているスクリプトホスト名を取得する。
	/// cscript, wscript, WScript の何れかが返ってくるはず。
	var script_host = function() {
		var fsys = WScript.CreateObject("Scripting.FileSystemObject");
		return fsys.GetBaseName(WScript.FullName);
	};

	WScript.Echo(script_host());
 })();

ところで関数script_host()は実行される度にデータを加工してスクリプトホスト名を返しているのだけど、よく考えたら自分自身(スクリプト)を実行しているホストが途中で変わることはあり得ない。なのでscript_host()を何度も実行するケースでは無駄が発生する気がする*4

そこで、

  1. 最初の1回だけスクリプトホスト名を取得する。
  2. 2回目以降は取得したスクリプトホスト名を使いまわす。

という古典的な方法にしてみる*5

(function() {
	var script_host;

	// 中略

	// スクリプトホスト名を取得していないので取得
	if (script_host === undefined) {
		script_host = (function() {
			var fsys = WScript.CreateObject("Scripting.FileSystemObject");
			return fsys.GetBaseName(WScript.FullName);
		})();
	}
	WScript.Echo(script_host);

	// 中略

	// 既に取得しているスクリプトホスト名使う
	WScript.Echo(script_host);
 })();

この方法には、幾つかマズイ点があると思う。

  1. 「最初にスクリプトホスト名が必要になる場所はどこか?」という、処理の流れを気にする必要がある。しかし処理の流れは大概変化するものだ。
  2. 処理の流れを気にしなくても良い風にしようにすると、今度はスクリプトホスト名を必要とする全ての場所で「script_host === undefined」みたいなチェックをすることになりかねない。
  3. 何よりscript_hostは単なる変数なので、誰かが何処かで変更してしまう可能性がある。

1、2は変数script_hostを定義する時点でスクリプトホスト名を取得してしまうことで回避できるけど、3についてはどうしようもない。

スクリプトを書く本人としては、

  • スクリプトホスト名が必要な所では何も考えず関数script_host()を使いたい。
  • script_hostが単なる変数だと、オペミスで何かやってしまいそうでイヤ。

といったわがままな要求を貫きたいところでもある。

要は、

  • 「script_host === undefined」みたいなチェックをscript_host()内部でやってしまう。
  • スクリプトホスト名を取得したら、その値をscript_host()以外の場所からアクセスすることが不可能な場所に保持しておく。

といった感じに、全てscript_host()の中に押し込んでしまいたいのだ。

ということでクロージャにしてみた。

// script_host3.js
(function() {
	/// このスクリプトを実行しているスクリプトホスト名を取得する。
	/// cscript, wscript, WScript の何れかが返ってくるはず。
	var script_host = (function() {
		var name;

		return function() {
			if (name === undefined) {
				name = (function() {
					var fsys = WScript.CreateObject("Scripting.FileSystemObject");
					return fsys.GetBaseName(WScript.FullName);
				})();
			}
			return name;
		};
	})();

	WScript.Echo(script_host());
	WScript.Echo(script_host());
 })();

このスクリプトでは、script_host()はスクリプトホスト名が必要になるまで何もしない。必要になったら、1回目はスクリプトホスト名を取得して、加工して保存する。2回目以降は保存した値をそのまま返す。

C/C++プログラマ的には、構造体やクラスを使ってステートマシンを書くのと似たような感じだと思う。script_host()内部の変数nameはprivateなメンバ変数といったところか。

スクリプトホスト名を予め取得しておく書き方もできる。例えばこんな感じ。

// script_host4.js
(function() {
	/// このスクリプトを実行しているスクリプトホスト名を取得する。
	/// cscript, wscript, WScript の何れかが返ってくるはず。
	var script_host = (function() {
		var fsys = WScript.CreateObject("Scripting.FileSystemObject");
		var name = fsys.GetBaseName(WScript.FullName);
		return function() { return name; };
	})();

	WScript.Echo(script_host());
 })();

script_host()を定義する時にスクリプトホスト名を取得してしまう。script_host()を実行すると、既に取得済みの値が返されることになる。

*1:実際に役に立っているから、色んな人がクロージャを使っている……のだと思うけど、間違ってないよね?

*2:もっともスクリプトホストの実行ファイル名自体を変更されたらアウトだけど。

*3:Windows XP Pro SP2上のWSH 5.6で確認。

*4:多分、殆ど気にする必要のない程度だろうけど。

*5:スクリプトホスト名を取得するよりも文字列を複製して参照を返す方が速い、という暗黙の前提がそこにはある。それが正しいかどうかは不明。