forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 4
/
lazy-evaluation.dd
314 lines (256 loc) · 7.82 KB
/
lazy-evaluation.dd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
Ddoc
$(D_S 関数引数の遅延評価,
<center>
$(I by Walter Bright, $(LINK http://www.digitalmars.com/d))
</center>
$(P 遅延評価とは、ある式を、その結果が本当に必要になる時点までは
評価しないでおくテクニックです。
論理演算子 &&, || や三項演算子 ?: は、
従来からある遅延評価を行う手法です:
)
---
void test(int* p)
{
if (p && p[0])
...
}
---
$(P 二番目の式 $(D p[0]) は $(D p)
$(B null) でないときに限り評価されます。
もし仮に二番目の式が遅延評価されないとすると、
$(D p) が $(B null) のときには実行時エラーとなってしまうでしょう。
)
$(P 遅延評価演算子は実に有益なものではありますが、同時に、無視できない制限も存在します。
ログ取り関数を考えてみましょう。メッセージのログをとるもので、
グローバルな設定値によって実行時に
ON/OFFを切り替えられるものとします:
)
---
void log(char[] message)
{
if (logging)
fwritefln(logfile, message);
}
---
$(P メッセージ文字列が実行時に作られることはよくあります:
)
---
void foo(int i)
{
log("Entering foo() with i set to " ~ toString(i));
}
---
$(P これは問題なく動作しますが、ログ取り機能のON/OFFにかかわらず
メッセージ文字列の構築が行われてしまうのが問題です。
ログ機能を頻繁に使うアプリケーションでは、
これはパフォーマンス上の重大な問題となる可能性があります。
)
$(P 解決策のひとつは、遅延評価をつかうことでしょう。
)
---
void foo(int i)
{
if (logging) log("Entering foo() with i set to " ~ toString(i));
}
---
$(P しかし、これはログ取り機能の詳細をユーザーにさらすことになり、
カプセル化の原則に反します。C言語では、この手の問題はよく
マクロを使って対処します:
)
$(CCODE
#define LOG(string) (logging && log(string))
)
$(P しかしこれも本質的な解決にはなっていません。プリプロセッサマクロには、
ご存じの通りいくつもの欠点があります:)
$(UL
$(LI 変数 $(D logging) がユーザーの名前空間に公開される)
$(LI マクロはデバッガに認識されない)
$(LI マクロはグローバルにしか使えず、スコープを切れない)
$(LI マクロはクラスのメンバにできない)
$(LI マクロはそのアドレスを取得することができず、
関数のように間接的に持ち回ることができない)
)
$(P 堅固な解決方法は、
関数引数の遅延評価を用いるものです。
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); });
}
---
$(P こうすると、文字列構築の式はloggingがtrueの時のみ実行されますし、
カプセル化も保たれています。唯一の問題点は、
式を $(D { return $(I exp); }) で囲むのがいまいち美しくないというところです。
)
$(P そこで D では、(Andrei Alexandrescu の提案による)
小さいけれど不可欠な一歩を進めました。
任意の式は、
$(D void) ないしはその式の型を返す delegate へと
暗黙変換できるようになっています。
(Tomasz Stachowiak の提案により、このdelegate宣言は
$(D lazy) 記憶域クラスを仮引数に指定する形式に変更されました。)
これを使うと先ほどの関数は:
)
---
void log(lazy char[] dg)
{
if (logging)
fwritefln(logfile, dg());
}
void foo(int i)
{
log("Entering foo() with i set to " ~ toString(i));
}
---
$(P 元々の形と全く同じになりました。しかし今度は、
メッセージ文字列はログ取り機能がONの時のみ構築されます。
)
$(P さて、似たようなパターンがコード中に繰り返されることはよくあります。
そのようなパターンを抽象化してカプセル化すれば、
コードの複雑性、ひいてはバグを減らすことにつながります。
このような抽象化のもっとも一般的な例は、
関数そのものです。
遅延評価は、他のパターンをカプセル化するための土台としても有効です。
)
$(P簡単な例として、ある式が $(I count)
回実行される、というものを考えてみましょう。パターンは:
)
---
for (int i = 0; i < count; i++)
exp;
---
$(P です。このパターンは、遅延評価を使うことで関数にカプセル化できます:
)
---
void dotimes(int count, lazy void exp)
{
for (int i = 0; i < count; i++)
exp();
}
---
$(P これは以下のように使用します:
)
---
void foo()
{
int x = 0;
dotimes(10, writef(x++));
}
---
$(P 出力結果はこうなります:
)
$(CONSOLE
0123456789
)
$(P もっと複雑なユーザー定義の制御構造を定義することも可能です:
以下は、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;
}
}
---
$(P 使用法は:
)
---
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"))
);
}
---
$(P 出力結果は:
)
$(CONSOLE
it is 2
)
$(P プログラミング言語 Lisp になじみのある方は、
Lisp のマクロとの類似に気づかれるかもしれません。
)
$(P 最後の例として、よくあるパターンを取り上げてみます:
)
---
Abc p;
p = foo();
if (!p)
throw new Exception("foo() failed");
p.bar(); // ここで p を使う
---
$(P throw は文であって式ではないため、この一連の処理を行う式を書こうとしても、
どうしても複数の文に分けて
余分な変数を導入する必要がでてきてしまいます。
(この問題についての綿密な考察が、Andrei Alexandrescu と
Petru Marginean の論文
$(LINK2 http://erdani.org/publications/cuj-06-2003.html, Enforcements)で為されています)。
遅延評価を使うと、これは全てひとつの関数に
カプセル化することができます:
)
---
Abc Enforce(Abc p, lazy char[] msg)
{
if (!p)
throw new Exception(msg());
return p;
}
---
$(P 先ほどの例は簡単に:
)
---
Enforce(foo(), "foo() failed").bar();
---
$(P と、5行から1行に減りました。Enforce は、テンプレート関数にすると
さらに有用です:
)
---
T Enforce(T)(T p, lazy char[] msg)
{
if (!p)
throw new Exception(msg());
return p;
}
---
<h2>まとめ</h2>
$(P 関数引数の遅延評価は、関数の表現力を劇的に拡張します。
さまざまな頻出コードパターンやイディオムを、
かつては綺麗にカプセル化できなかったものでも、
うまくまとめあげることができます。
)
<h2>謝辞</h2>
$(P Andrei Alexandrescu, Bartosz Milewski, David Held
のひらめきと力添えに、厚く感謝いたします。
また、D言語コミュニティからの建設的な批判
(例えば Tomasz Stachowiak の投稿 $(NG_digitalmars_D 41633) から始まるスレッドなど)
が、大きな助けとなっています。
)
)
Macros:
TITLE=関数引数の遅延評価
WIKI=LazyEvaluation
CATEGORY_ARTICLES=$0
NG_digitalmars_D = <a href="http://www.digitalmars.com/pnews/read.php?server=news.digitalmars.com&group=digitalmars.D&artnum=$0">D/$0</a>