forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
lazy-evaluation.dd
314 lines (256 loc) · 6.41 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 Lazy Evaluation of Function Arguments,
<center>
$(I by Walter Bright, $(LINK http://www.digitalmars.com/d))
</center>
$(P Lazy evaluation is the technique of not evaluating an expression
unless and until the result of the expression is required.
The &&, || and ?: operators are the conventional way to
do lazy evaluation:
)
---
void test(int* p)
{
if (p && p[0])
...
}
---
$(P The second expression $(D p[0]) is not evaluated unless $(D p)
is not $(B null).
If the second expression was not lazily evaluated, it would
generate a runtime fault if $(D p) was $(B null).
)
$(P While invaluable, the lazy evaluation operators have significant
limitations. Consider a logging function, which logs
a message, and can be turned on and off at runtime based on a global
value:
)
---
void log(char[] message)
{
if (logging)
fwritefln(logfile, message);
}
---
$(P Often, the message string will be constructed at runtime:
)
---
void foo(int i)
{
log("Entering foo() with i set to " ~ toString(i));
}
---
$(P While this works, the problem is that the building of the message
string happens regardless of whether logging is enabled or not.
With applications that make heavy use of logging, this can become
a terrible drain on performance.
)
$(P One way to fix it is by using lazy evaluation:
)
---
void foo(int i)
{
if (logging) log("Entering foo() with i set to " ~ toString(i));
}
---
$(P but this violates encapsulation principles by exposing the details
of logging to the user. In C, this problem is often worked around
by using a macro:
)
$(CCODE
#define LOG(string) (logging && log(string))
)
$(P but that just papers over the problem. Preprocessor macros have
well known shortcomings:)
$(UL
$(LI The $(D logging) variable is exposed in the user's namespace.)
$(LI Macros are invisible to symbolic debuggers.)
$(LI Macros are global only, and cannot be scoped.)
$(LI Macros cannot be class members.)
$(LI Macros cannot have their address taken, so cannot be passed indirectly
like functions can.)
)
$(P A robust solution would be
a way to do lazy evaluation of function parameters. Such a way
is possible in the D programming language using a delegate parameter:
)
---
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 Now, the string building expression only gets evaluated if logging
is true, and encapsulation is maintained. The only trouble is that
few are going to want to wrap expressions with $(D { return $(I exp); }).
)
$(P So D takes it one small, but crucial, step further
(suggested by Andrei Alexandrescu).
Any expression
can be implicitly converted to a delegate that returns either $(D void) or
the type of the expression.
The delegate declaration is replaced by the $(D lazy) storage class
(suggested by Tomasz Stachowiak).
The functions then become:
)
---
void log(lazy char[] dg)
{
if (logging)
fwritefln(logfile, dg());
}
void foo(int i)
{
log("Entering foo() with i set to " ~ toString(i));
}
---
$(P which is our original version, except that now the string is not
constructed unless logging is turned on.
)
$(P Any time there is a repeating pattern seen in code, being able to
abstract out that pattern and encapsulate it means we can reduce the
complexity of the code, and hence bugs. The most common example of
this is the function
itself.
Lazy evaluation enables encapsulation of a host of other patterns.
)
$(P For a simple example, suppose an expression is to be evaluated $(I count)
times. The pattern is:
)
---
for (int i = 0; i < count; i++)
exp;
---
$(P This pattern can be encapsulated in a function using lazy evaluation:
)
---
void dotimes(int count, lazy void exp)
{
for (int i = 0; i < count; i++)
exp();
}
---
$(P It can be used like:
)
---
void foo()
{
int x = 0;
dotimes(10, writef(x++));
}
---
$(P which will print:
)
$(CONSOLE
0123456789
)
$(P More complex user defined control structures are possible.
Here's a method to create a switch like structure:
)
---
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;
}
}
---
$(P which can be used like:
)
---
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 which will print:
)
$(CONSOLE
it is 2
)
$(P Those familiar with the Lisp programming language will notice some
intriguing parallels with Lisp macros.
)
$(P For a last example, there is the common pattern:
)
---
Abc p;
p = foo();
if (!p)
throw new Exception("foo() failed");
p.bar(); // now use p
---
$(P Because throw is a statement, not an expression, expressions that
need to do this need to be broken up into multiple statements,
and extra variables are introduced.
(For a thorough treatment of this issue, see Andrei Alexandrescu and
Petru Marginean's paper
$(LINK2 http://erdani.org/publications/cuj-06-2003.html, Enforcements)).
With lazy evaluation, this can all be encapsulated into a single
function:
)
---
Abc Enforce(Abc p, lazy char[] msg)
{
if (!p)
throw new Exception(msg());
return p;
}
---
$(P and the opening example above becomes simply:
)
---
Enforce(foo(), "foo() failed").bar();
---
$(P and 5 lines of code become one. Enforce can be improved by making it a
template function:
)
---
T Enforce(T)(T p, lazy char[] msg)
{
if (!p)
throw new Exception(msg());
return p;
}
---
$(H2 Conclusion)
$(P Lazy evaluation of function arguments dramatically extends the expressive
power of functions. It enables the encapsulation into functions of many
common coding patterns and idioms that previously were too clumsy or
impractical to do.
)
$(H2 Acknowledgements)
$(P I gratefully acknowledge the inspiration and assistance
of Andrei Alexandrescu, Bartosz Milewski, and David Held.
The D community helped a lot with much constructive
criticism, such as the thread starting with
Tomasz Stachowiak in $(NG_digitalmars_D 41633).
)
)
Macros:
TITLE=LazyEvaluationOfFunctionArguments
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>