英語版
このページの英語版を見る

関数引数の遅延評価

ウォルター・ブライト著、http://www.digitalmars.com/d

遅延評価とは、式の結果が必要になるまでその式を評価しない手法である。 &&、||、?:演算子は、遅延評価を行うための一般的な方法である。

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

2番目の式p[0] は、pnull でない限り評価されない。 もし2番目の式が遅延評価されなかった場合、pnull であった場合、実行時にエラーが発生する。

非常に便利な遅延評価演算子だが、重大な 制限がある。例えば、メッセージをログに記録するログ記録関数を考えてみよう。 この関数は、グローバル値に基づいて実行時にオン/オフを切り替えることができる。

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

メッセージ文字列は実行時に構築されることが多い。

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

これは動作するが、問題はメッセージ文字列の構築が、 ロギングが有効になっているか否かに関わらず発生することだ。 ロギングを多用するアプリケーションでは、 パフォーマンスに深刻な影響を及ぼす可能性がある。

これを修正する方法のひとつは、遅延評価を使用することである。

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

しかし、これはカプセル化の原則に違反し、 ユーザーにロギングの詳細を公開することになる。C言語では、この問題はマクロを使用することで回避されることが多い。

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

しかし、それは問題を覆い隠しているだけである。プリプロセッサマクロには よく知られた欠点がある:

強固なソリューションは、 関数パラメータの遅延評価を行う方法である。このような方法は、 プログラミング言語 D では、デリゲートパラメータを使用することで可能である。

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

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

文字列構築式は、ロギングが trueの場合のみ評価され、カプセル化が維持される。唯一の問題は、 式をラップしたいと思う人がほとんどいないことだ { return exp; }

そこでDは、さらに一歩、小さくても重要なステップを踏み出す (Andrei Alexandrescuの提案による)。 任意の式は、void または式の型を返すデリゲートに暗黙的に変換できる。 デリゲート宣言は、lazy ストレージクラスに置き換えられる (Tomasz Stachowiakの提案による)。 関数は以下のようになる:

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

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

これは、ログ記録が有効になっていない限り文字列が構築されないことを除いて、私たちのオリジナルバージョンである。

コードに繰り返しパターンが見られる場合、 そのパターンを抽象化してカプセル化できるということは、 コードの複雑性を低減でき、したがってバグも低減できることを意味する。 最も一般的な例は、関数 そのものである。 遅延評価により、他の多くのパターンのカプセル化が可能になる。

簡単な例として、ある式がカウント回数だけ評価されると仮定しよう。 そのパターンは次の通りである。

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, write(x++));
}

これは次のように表示される。

0123456789

より複雑なユーザー定義の制御構造も可能である。 以下は、switchのような構造を作成する方法である。

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

/* ここでは可変長引数がデリゲートに
   変換されている。
 */
void cond(bool delegate()[] cases ...)
{
    foreach (c; cases)
    {
        if (c())
            break;
    }
}

これは次のように使用できる。

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

これは次のように表示される。

it is 2

Lispプログラミング言語に詳しい方なら、 Lispマクロとの興味深い類似点に気づくことだろう。

最後の例として、よくあるパターンがある。

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

遅延評価を使用すれば、これらすべてを単一の関数にカプセル化できる。

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

そして、最初の例は単純に次のようになる。

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

5行のコードが1行になる。Enforceは、テンプレート化された関数にすることで改善できる。

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

結論

関数引数の遅延評価は、関数の表現力を飛躍的に向上させる。 これにより、これまで扱いにくかったり、実用的でなかったりした多くの 一般的なコーディングパターンやイディオムを関数にカプセル化することが 可能になる。

謝辞

私は、Andrei Alexandrescu、Bartosz Milewski、David Heldのインスピレーションと支援に感謝している。 Dコミュニティは、多くの建設的な批判で大いに助けてくれた。 例えば、Tomasz Stachowiakによるスレッドdigitalmars.D&artnum=41633">D/41633 などである。