怠惰な評価は、式を評価しない技術です式の結果が必要になるまで、unlessとなります。&&,|/と?:演算子は、遅延評価のための従来の方法です:
void test(int* p){ if (p && p) ...}
2番目の式pは、pis not nullでない限り評価されません。二番目の式が遅延評価されなかった場合、pがnullであった場合、実行時障害が発生します。
遅延評価演算子は非常に重要ですが、遅延評価演算子は重要な制限を持っています。 メッセージをログに記録し、globalvalueに基づいて実行時にオンとオフを切り替えることができるロギング関数を考えてみましょう:
void log(const(char) message){ if (logging) fwritefln(logfile, message);}
多くの場合、メッセージ文字列は実行時に構築されます:
void foo(int i){ log("Entering foo() with i set to " ~ toString(i));}
これは機能しますが、問題は、ログが有効になっているかどうかにかかわらず、messagestringの構築が行われることです。ロギングを多用するアプリケーションでは、これはパフォーマンスにひどいドレインになる可能性があります。
それを修正する一つの方法は、遅延評価を使用することです:
void foo(int i){ if (logging) log("Entering foo() with i set to " ~ toString(i));}
しかし、これはロギングの詳細をユーザーに公開することによってカプセル化の原則に違反します。 Cでは、この問題はマクロを使用して回避されることがよくあります:
#define LOG(string) (logging && log(string))
しかし、それは問題の上にちょうど論文。 プリプロセッサマクロには既知の欠点があります:
- logging変数はユーザーの名前空間で公開されます。
- マクロはシンボリックデバッガーには見えません。
- マクロはグローバルのみであり、スコープは設定できません。
- マクロはクラスメンバーにすることはできません。
- マクロはアドレスを取得できないため、関数のように間接的に渡すことはできません。
堅牢な解決策は、関数パラメータの遅延評価を行う方法です。 このような方法は、デリゲートパラメータを使用して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); });}
これで、文字列構築式は、loggingis trueの場合にのみ評価され、カプセル化が維持されます。 唯一の問題は、fewが{return exp;}で式をラップしたいということです。
だから、Dはそれを小さく、しかし重要な、さらに一歩(Andrei Alexandrescuによって提案された)。任意の式は、voidまたは式の型のいずれかを返すデリゲートに暗黙的に変換できます。デリゲート宣言は、遅延ストレージクラス(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));}
これは私たちの元のバージョンですが、ロギングがオンになっていない限り、文字列はnotconstructedになりました。
コードに繰り返しパターンが見られるときはいつでも、そのパターンを抽象化してカプセル化できることは、コードの複雑さ、ひいてはバグを減らすこと 最も一般的な例は、これは機能ですそれ自体。遅延評価は、他のパターンのホストのカプセル化を可能にします。
簡単な例として、式がcounttimes評価されるとします。 パターンは次のとおりです:
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
より複雑なユーザー定義の制御構造が可能です。スイッチのような構造を作成する方法は次のとおりです:
bool scase(bool b, lazy void dg){ if (b) dg(); return b;}/* Here the variadic arguments are converted to delegates in this special case. */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(); // now use p
throwは式ではなく文であるため、これを行う必要がある式は複数の文に分割する必要があり、余分な変数が導入されます。(この問題の徹底的な治療については、Andrei Alexandrescu andPetru MargineanのpaperEnforcementsを参照してください)。遅延評価では、これはすべてsinglefunctionにカプセル化することができます:
Abc Enforce(Abc p, lazy const(char) msg){ if (!p) throw new Exception(msg()); return p;}
そして、上記の開口部の例は単純になります:
Enforce(foo(), "foo() failed").bar();
そして、5行のコードが一つになります。 にすることで改善することができます。:
T Enforce(T)(T p, lazy const(char) msg){ if (!p) throw new Exception(msg()); return p;}
結論
関数引数の遅延評価は、関数の表現力を劇的に拡張します。 これは、以前に行うにはあまりにも不器用だった多くの一般的な符号化パターンやイディオムの機能にカプセル化を可能にします。
謝辞
私はAndrei Alexandrescu、Bartosz Milewski、David Heldのインスピレーションと支援を感謝しています。 Dコミュニティは、D/41633のTomasz Stachowiakで始まるスレッドのように、多くの建設的な批判で多くのことを助けました。