-
Notifications
You must be signed in to change notification settings - Fork 3
/
TransparentLazy.pm
309 lines (257 loc) · 8.72 KB
/
TransparentLazy.pm
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
#
# Copyright (c) 2015-2021 Christian Jaeger, [email protected]
#
# This is free software, offered under either the same terms as perl 5
# or the terms of the Artistic License version 2 or the terms of the
# MIT License (Expat version). See the file COPYING.md that came
# bundled with this file.
#
=head1 NAME
FP::TransparentLazy - lazy evaluation with transparent evaluation
=head1 SYNOPSIS
use FP::TransparentLazy;
# This is the same SYNOPSIS as in FP::Lazy but with most `force`
# calls removed, and slightly differing behaviour in places
# (e.g. `$a + 2` will evaluate the thunk here and thus give
# division by zero):
my $a = lazy { 1 / 0 };
eval {
# $a's evaluation is forced here
print $a
};
like $@, qr/^Illegal division by zero/;
eval {
$a + 2
};
like $@, qr/^Illegal division by zero/;
my $count = 0;
my $b = lazy { $count++; 1 / 2 };
is is_promise($b), 1;
is $count, 0;
is $b, 1/2; # increments $count
is $count, 1;
# $b is still a promise at this point (although an evaluated one):
is is_promise($b), 1;
is $b, 1/2; # does not increment $count anymore
is $count, 1;
# The following stores result of `force $b` back into $b
FORCE $b;
is is_promise($b), undef;
is $b, 1/2;
is $count, 1;
# Note that lazy evaluation and mutation usually doesn't mix well -
# lazy programs better be purely functional. Here $tot depends not
# just on the inputs, but also on how many elements were evaluated:
use FP::Stream qw(stream_map); # uses `lazy` internally
use FP::List;
my $tot = 0;
my $l = stream_map sub {
my ($x) = @_;
$tot += $x;
$x*$x
}, list (5,7,8);
is $tot, 0;
is $l->first, 25;
is $tot, 5;
is $l->length, 3;
is $tot, 20;
# Also note that `local` does mutation (even if in a somewhat
# controlled way):
our $foo = "";
sub moo {
my ($bar) = @_;
local $foo = "Hello";
lazy { "$foo $bar" }
}
is moo("you")->force, " you";
is moo("you"), " you";
# runtime conditional lazyness:
sub condprom {
my ($cond) = @_;
lazy_if { 1 / 0 } $cond
}
ok is_promise(condprom 1);
eval {
# immediate division by zero exception (still pays
# the overhead of two subroutine calls, though)
condprom 0
};
like $@, qr/^Illegal division by zero/;
# A `lazyLight` promise is re-evaluated on every access:
my $z = 0;
my $v = lazyLight { $z++; 3*4 };
is $v, 12;
is $z, 1;
is $v, 12;
is $z, 2;
is force($v), 12;
is $z, 3;
is $v, 12;
is $z, 4;
# There are 3 possible motivations for lazyLight: (1) lower
# allocation cost (save the wrapper data structure); (2) no risk
# for circular references (due to storing the result back into the
# wrapper (mutation) that can be used recursively); (3) to get
# fresh re-evaluation on every access and thus picking up any
# potential side effect.
# Arguably (3) is against the functional programming idea, and is
# a bit of a mis-use of lazyLight. But, at least for now,
# FP::TransparentLazy helps this case by not using `FORCE`
# transparently; it shouldn't since that would break automatic
# stream_ detection on subsequent calls (to things like `->map`
# instead of `->stream_map`).
# Note that manual use of `FORCE` still stops the re-evalution:
ok ref $v;
is FORCE($v), 12;
is $z, 5;
is $v, 12;
is $z, 5; # you can see that re-evaluation has stopped
ok not ref $v;
# WARNING: such impure lazyLight promises from TransparentLazy are
# dangerous in that if you never explicitly `force` them and use
# the result (or `FORCE` them) then the exposure to side effects
# will remain active. Use FP::Lazy's lazyLight instead, which
# requires forcing thus requires this boundary to be explicit.
=head1 DESCRIPTION
This implements a variant of FP::Lazy that forces promises
automatically upon access (and writes their result back to the place
they are forced from, like FP::Lazy's `FORCE` does, except in the
lazyLight case where `FORCE` is consciously not used automatically to
keep more consistent re-evaluation behaviour). Otherwise the two are
fully interchangeable.
NOTE: this is EXPERIMENTAL. Also, should this be merged with
L<Data::Thunk>? OTOH, should remain interchangeable with L<FP::Lazy>,
and maybe merged with that one.
The drawback of transparency might be more confusion, as it's not
directly visible anymore (neither in the debugger nor the source code)
what's lazy. Also, transparent forcing will be a bit more expensive
CPU wise. Please give feedback about your experiences!
=head1 SEE ALSO
L<FP::Lazy>
=head1 NOTE
This is alpha software! Read the status section in the package README
or on the L<website|http://functional-perl.org/>.
=cut
package FP::TransparentLazy;
use strict;
use warnings;
use warnings FATAL => 'uninitialized';
use Exporter "import";
our @EXPORT = qw(lazy lazy_if lazyLight force FORCE is_promise);
our @EXPORT_OK = qw(delay lazy_backtrace);
our %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]);
use FP::Lazy qw(force FORCE is_promise lazy_backtrace); # for re-export
our $eager = ($ENV{DEBUG_FP_LAZY} and $ENV{DEBUG_FP_LAZY} =~ /^eager$/i);
our $debug = $ENV{DEBUG_FP_LAZY} ? (not $eager) : '';
sub lazy (&) {
$eager
? goto $_[0]
: bless [$_[0], undef, $debug && FP::Repl::Stack->get(1)->backtrace],
"FP::TransparentLazy::Promise"
}
sub lazy_if (&$) {
(
($_[1] and not $eager)
? bless([$_[0], undef, $debug && FP::Repl::Stack->get(1)->backtrace],
"FP::TransparentLazy::Promise")
: do {
my ($thunk) = @_;
@_ = ();
goto $thunk;
}
)
}
# not providing for caching (1-time-only evaluation)
sub lazyLight (&) {
$eager ? goto $_[0] : bless $_[0], "FP::TransparentLazy::PromiseLight"
}
sub delay (&);
*delay = \&lazy;
sub delayLight (&);
*delayLight = \&lazyLight;
# XX to make it truly transparent, should always overload '&{}'; but
# then how to force it without getting into an infinite loop? No way
# to turn off the overload (except reblessing)?
# XX hm, can't overload '@{}', why?
sub overloads {
my ($with_application_overload) = @_;
($with_application_overload ? ('&{}') : ()), qw'"" 0+ bool qr ${} %{} *{}';
}
# COPY-PASTE from FP::Lazy
sub die_type_error {
my ($expected, $gotstr, $v) = @_;
die "promise expected to evaluate to an object "
. "of class '$expected' but got $gotstr: "
. show($v)
}
# Only for the overload, you shouldn't use this manually (for one,
# because it doesn't check the number of arguments, which is because
# overload passes 3 of them, and then because this can't be used for
# other promises and that will be dangerous):
sub forceTransparentLazy {
my ($perhaps_promise) = @_;
my $nocache = 0;
# COPY-PASTE from part of FP::Lazy::force:
if (defined(my $thunk = $$perhaps_promise[0])) {
my $v = force(&$thunk(), $nocache);
if ($$perhaps_promise[2]) {
if (defined(my $got = blessed($v))) {
$v->isa($$perhaps_promise[2])
or die_type_error($$perhaps_promise[2], "a '$got'", $v);
} else {
die_type_error($$perhaps_promise[2], "a non-object", $v);
}
}
unless ($nocache) {
$$perhaps_promise[1] = $v;
$$perhaps_promise[0] = undef;
}
$v
} else {
$$perhaps_promise[1]
}
}
package FP::TransparentLazy::Promise {
our @ISA = qw(FP::Lazy::Promise);
# Use of the FORCE method would be bad since it will make stream_
# detection fail on subsequent calls! (OK, would need to use
# `Keep` anyway usually, but apparently not always?) Also, can't
# use "force" method as that expects 1-2 arguments, overload
# passes 3 (different ones)!
use overload(
(
map { $_ => \&FP::TransparentLazy::forceTransparentLazy }
FP::TransparentLazy::overloads(1)
),
fallback => 1
);
}
# Do *not* call "FORCE" method for PromiseLight if the aim is to
# re-evaluate it every time.
sub forceLight {
&{ $_[0] }
}
package FP::TransparentLazy::PromiseLight {
our @ISA = qw(FP::Lazy::PromiseLightBase);
use overload(
(
map { $_ => \&FP::TransparentLazy::forceLight }
FP::TransparentLazy::overloads(0)
),
fallback => 1
);
}
use Chj::TEST;
our $c;
TEST {
$c = lazy {
sub {"foo"}
};
ref $c
}
'FP::TransparentLazy::Promise';
TEST { &$c() }
"foo";
TEST { ref $c }
"FP::TransparentLazy::Promise"; # was CODE before removing transparent FORCE
1