D 1.0   D 2.0
About Japanese Translation

Last update Wed Oct 4 19:45:07 2006

関数引数の遅延評価

by Walter Bright, http://www.digitalmars.com/d

遅延評価とは、ある式を、その結果が本当に必要になる時点までは 評価しないでおくテクニックです。 論理演算子 &&, || や三項演算子 ?: は、 従来からある遅延評価を行う手法です:

void test(int* p)
{
    if (p && p[0])
	...
}

二番目の式 p[0] は、pnull でないときに限り評価されます。 もし仮に二番目の式が遅延評価されないとすると、 pnull のときには実行時エラーとなってしまうでしょう。

遅延評価演算子は実に有益なものではありますが、同時に、無視できない制限も存在します。 ログ取り関数を考えてみましょう。メッセージのログをとるもので、 グローバルな設定値によって実行時に ON/OFFを切り替えられるものとします:

void log(char[] message)
{
    if (logging)
	fwritefln(logfile, message);
}

メッセージ文字列が実行時に作られることはよくあります:

void foo(int i)
{
    log("Entering foo() with i set to " ~ toString(i));
}

これは問題なく動作しますが、ログ取り機能のON/OFFにかかわらず メッセージ文字列の構築が行われてしまうのが問題です。 ログ機能を頻繁に使うアプリケーションでは、 これはパフォーマンス上の重大な問題となるかのうせいがあります。

解決策のひとつは、遅延評価をつかうことでしょう。

void foo(int i)
{
    if (logging) log("Entering foo() with i set to " ~ toString(i));
}

しかし、これはログ取り機能の詳細をユーザーにさらすことになり、 カプセル化の原則に反します。C言語では、この手の問題はよく マクロを使って対処します:

#define LOG(string)  (logging && log(string))

しかしこれも本質的な解決にはなっていません。プリプロセッサマクロには、 ご存じの通りいくつもの欠点があります:

堅固な解決方法は、 関数引数の遅延評価を用いるものです。 D言語では、引数をdelegateにすることでこれが実現できます:

void log(char[] delegate() dg)
{
    if (logging)
	fwritefln(logfile, dg());
}

void foo(int i)
{
    log( { return "Entering foo() with i set to " ~ toString(i); });
}

こうすると、文字列構築の式はloggingがtrueの時のみ実行されますし、 カプセル化も保たれています。唯一の問題点は、 式を { return exp; } で囲むのがいまいち好ましくないというところです。

そこで D では、(Andrei Alexandrescu の提案による) 小さいけれど不可欠な一歩を進めました。 任意の式は、 void ないしはその式の型を返す delegate へと 暗黙変換できるようになっています。 (Tomasz Stachowiak の提案により、このdelegate宣言は lazy 記憶域クラスを仮引数に指定する形式に変更されました。) これを使うと foo 関数は:

void log(lazy char[] dg)
{
    if (logging)
	fwritefln(logfile, dg());
}

void foo(int i)
{
    log("Entering foo() with i set to " ~ toString(i));
}

元々の形と全く同じになりました。しかし今度は、 メッセージ文字列はログ取り機能がONの時のみ構築されます。

さて、似たようなパターンがコード中に繰り返されることはよくあります。 そのようなパターンを抽象化してカプセル化すれば、 コードの複雑性、ひいてはバグを減らすことにつながります。 このような抽象化のもっとも一般的な例は、 関数そのものです。 遅延評価は、他のパターンをカプセル化するための土台としても有効です。

簡単な例として、ある式が count 回実行される、というものを考えてみましょう。パターンは:

for (int i = 0; i < count; i++)
   exp;

です。このパターンは、遅延評価を使うことで関数にカプセル化できます:

void dotimes(int count, lazy void exp)
{
    for (int i = 0; i < count; i++)
       exp();
}

これは以下のように使用します:

void foo()
{
    int x = 0;
    dotimes(10, writef(x++));
}

出力結果はこうなります:

0123456789

もっと複雑なユーザー定義の制御構造を定義することも可能です: 以下は、switchに似た構造を作る例です:

bool scase(bool b, lazy void dg)
{
    if (b)
	dg();
    return b;
}

/* 可変個引数の場合は特殊ケースとして、
   delegateでない実引数もdelegateに暗黙変換されます。
 */
void cond(bool delegate()[] cases ...)
{
    foreach (c; cases)
    {	if (c())
	    break;
    }
}

使用法は:

void foo()
{
    int v = 2;
    cond
    (
	scase(v == 1, writefln("it is 1")),
	scase(v == 2, writefln("it is 2")),
	scase(v == 3, writefln("it is 3")),
	scase(true,   writefln("it is the default"))
    );
}

出力結果は:

it is 2

プログラミング言語 Lisp になじみのある方は、 Lisp のマクロとの類似に気づかれるかもしれません。

最後の例として、よくあるパターンを取り上げてみます:

Abc p;
p = foo();
if (!p)
    throw new Exception("foo() failed");
p.bar();	// ここで p を使う

throw は文であって式ではないため、この一連の処理を行う式を書こうとしても、 どうしても複数の文に分けて 余分な変数を導入する必要がでてきてしまいます。 (この問題についての綿密な考察が、Andrei Alexandrescu と Petru Marginean の論文 Enforcements で為されています)。 遅延評価を使うと、これは全てひとつの関数に カプセル化することができます:

Abc Enforce(Abc p, lazy char[] msg)
{
    if (!p)
	throw new Exception(msg());
    return p;
}

先ほどの例は簡単に:

Enforce(foo(), "foo() failed").bar();

と、5行から1行に減りました。Enforce は、テンプレート関数にすると さらに有用です:

T Enforce(T)(T p,  lazy char[] msg)
{
    if (!p)
	throw new Exception(msg());
    return p;
}

まとめ

関数引数の遅延評価は、関数の表現力を劇的に拡張します。 さまざまな頻出コードパターンやイディオムを、 かつては綺麗にカプセル化できなかったものでも、 うまくまとめあげることができます。

謝辞

Andrei Alexandrescu, Bartosz Milewski, David Held のひらめきと力添えに、厚く感謝いたします。 また、D言語コミュニティからの建設的な批判 (例えば Tomasz Stachowiak の投稿 D/41633 から始まるスレッドなど) が、大きな助けとなっています。