Skip to content

Commit 98294fc

Browse files
izeigermanjbgi
authored andcommitted
Introduce the Eval monad
a convenient wrapper around Trampoline and P1.hardMemo for stack-safe computation.
1 parent 34f2c17 commit 98294fc

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package fj.data;
2+
3+
import fj.F;
4+
import fj.F0;
5+
import fj.P;
6+
import fj.P1;
7+
import fj.control.Trampoline;
8+
9+
/**
10+
* <code>Eval</code> is an abstraction over different models of evaluation.
11+
* The data constructors:
12+
* <ul>
13+
* <li><code>Now</code> - the value is evaluated immediately.</li>
14+
* <li><code>Later</code> - the value is evaluated only once when it's requested (lazy evaluation).</li>
15+
* <li><code>Always</code> - the value is evaluated every time when it's requested.</li>
16+
* </ul>
17+
*
18+
* Both <code>Later</code> and <code>Always</code> are lazy computations, while <code>Now</code> is eager.
19+
*
20+
*
21+
* @version %build.number%
22+
*/
23+
public abstract class Eval<A> {
24+
25+
/**
26+
* Constructs an eager evaluation by wrapping the given value.
27+
*
28+
* @param a the evaluated value.
29+
* @return an eval with computed value.
30+
*/
31+
public static <A> Eval<A> now(A a) {
32+
return new Now<>(a);
33+
}
34+
35+
/**
36+
* Constructs a lazy evaluation with caching.
37+
*
38+
* @param a the supplier that evaluates a value.
39+
* @return a lazy evaluation.
40+
*/
41+
public static <A> Eval<A> later(F0<A> a) {
42+
return new Later<>(a);
43+
}
44+
45+
/**
46+
* Constructs a lazy evaluation without caching.
47+
*
48+
* @param a the supplier that evaluates a value.
49+
* @return a lazy evaluation.
50+
*/
51+
public static <A> Eval<A> always(F0<A> a) {
52+
return new Always<>(a);
53+
}
54+
55+
/**
56+
* Constructs a lazy evaluation of an expression that produces <code>Eval<A></code>.
57+
* This operation is stack-safe and can be used for recursive computations.
58+
*
59+
* @param a the supplier that produces an <code>Eval<A></code>.
60+
* @return a lazily evaluated nested evaluation.
61+
*/
62+
public static <A> Eval<A> defer(F0<Eval<A>> a) {
63+
return new DeferEval<>(a);
64+
}
65+
66+
/**
67+
* Evaluates the computation and return its result.
68+
* Depending on whether the current instance is lazy or eager the
69+
* computation may or may not happen at this point.
70+
*
71+
* @return a result of this computation.
72+
*/
73+
public abstract A value();
74+
75+
/**
76+
* Transforms <code>Eval<A></code> into a <code>Eval<B></code> using
77+
* the given function.
78+
*
79+
* Note: the computation of the given transformation is always lazy,
80+
* even if it invoked for an eager <code>Now</code> instance. This computation
81+
* is performed in O(1) stack space.
82+
*
83+
* @param f the transformation function.
84+
* @return a transformed evaluation.
85+
*/
86+
public final <B> Eval<B> map(final F<A, B> f) {
87+
return bind(a -> now(f.f(a)));
88+
}
89+
90+
/**
91+
* Alias for {@link #bind(F)}.
92+
*/
93+
public final <B> Eval<B> flatMap(final F<A, Eval<B>> f) {
94+
return bind(f);
95+
}
96+
97+
/**
98+
* Transforms <code>Eval<A></code> into a <code>Eval<B></code> using
99+
* the given function that directly produces <code>Eval<B></code>.
100+
*
101+
* Note: the computation of the given transformation is always lazy,
102+
* even if it invoked for an eager <code>Now</code> instance. This computation
103+
* is performed in O(1) stack space.
104+
*
105+
* @param f the transformation function.
106+
* @return a transformed evaluation.
107+
*/
108+
public final <B> Eval<B> bind(final F<A, Eval<B>> f) {
109+
return new BindTrampolineEval<>(f, asTrampoline());
110+
}
111+
112+
/**
113+
* Transforms the current instance into a trampoline instance.
114+
*/
115+
abstract TrampolineEval<A> asTrampoline();
116+
117+
/**
118+
* Represents an eager computation.
119+
*/
120+
private static final class Now<A> extends Eval<A> {
121+
private final A a;
122+
123+
Now(A a) {
124+
this.a = a;
125+
}
126+
127+
@Override
128+
public final A value() {
129+
return a;
130+
}
131+
132+
@Override
133+
final TrampolineEval<A> asTrampoline() {
134+
return new PureTrampolineEval<>(this);
135+
}
136+
}
137+
138+
/**
139+
* Represents a lazy computation that is evaluated only once.
140+
*/
141+
private static final class Later<A> extends Eval<A> {
142+
private final P1<A> memo;
143+
144+
Later(F0<A> producer) {
145+
this.memo = P.hardMemo(producer);
146+
}
147+
148+
@Override
149+
public final A value() {
150+
return memo._1();
151+
}
152+
153+
@Override
154+
final TrampolineEval<A> asTrampoline() {
155+
return new PureTrampolineEval<>(this);
156+
}
157+
}
158+
159+
/**
160+
* Represents a lazy computation that is evaluated every time when it's requested.
161+
*/
162+
private static final class Always<A> extends Eval<A> {
163+
private final F0<A> supplier;
164+
165+
Always(F0<A> supplier) {
166+
this.supplier = supplier;
167+
}
168+
169+
@Override
170+
public final A value() {
171+
return supplier.f();
172+
}
173+
174+
@Override
175+
final TrampolineEval<A> asTrampoline() {
176+
return new PureTrampolineEval<>(this);
177+
}
178+
}
179+
180+
/**
181+
* A helper abstraction that allows to perform recursive lazy transformations in O(1) stack space.
182+
*/
183+
private static abstract class TrampolineEval<A> extends Eval<A> {
184+
185+
protected abstract Trampoline<A> trampoline();
186+
187+
@Override
188+
public final A value() {
189+
return trampoline().run();
190+
}
191+
192+
@Override
193+
final TrampolineEval<A> asTrampoline() {
194+
return this;
195+
}
196+
}
197+
198+
private static final class PureTrampolineEval<A> extends TrampolineEval<A> {
199+
private final Eval<A> start;
200+
201+
PureTrampolineEval(Eval<A> start) {
202+
this.start = start;
203+
}
204+
205+
@Override
206+
protected final Trampoline<A> trampoline() {
207+
return Trampoline.suspend(P.lazy(() -> Trampoline.pure(start.value())));
208+
}
209+
}
210+
211+
private static final class BindTrampolineEval<A, B> extends TrampolineEval<B> {
212+
private final TrampolineEval<A> next;
213+
private final F<A, Eval<B>> f;
214+
215+
BindTrampolineEval(F<A, Eval<B>> f, TrampolineEval<A> next) {
216+
this.next = next;
217+
this.f = f;
218+
}
219+
220+
@Override
221+
protected final Trampoline<B> trampoline() {
222+
return Trampoline.suspend(P.lazy(() -> next.trampoline().bind(v -> f.f(v).asTrampoline().trampoline())));
223+
}
224+
}
225+
226+
private static final class DeferEval<A> extends TrampolineEval<A> {
227+
private final P1<Eval<A>> memo;
228+
229+
DeferEval(F0<Eval<A>> producer) {
230+
this.memo = P.hardMemo(producer);
231+
}
232+
233+
@Override
234+
protected final Trampoline<A> trampoline() {
235+
return memo._1().asTrampoline().trampoline();
236+
}
237+
}
238+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package fj.data;
2+
3+
import fj.F0;
4+
import org.junit.Assert;
5+
import org.junit.Test;
6+
7+
public class EvalTest {
8+
9+
@Test
10+
public void testNow() {
11+
Eval<Integer> eval = Eval.now(1);
12+
Assert.assertEquals(eval.value().intValue(), 1);
13+
Assert.assertEquals(eval.map(a -> a.toString()).value(), "1");
14+
Assert.assertEquals(eval.bind(a -> Eval.now(a * 3)).value().intValue(), 3);
15+
}
16+
17+
@Test
18+
public void testLater() {
19+
InvocationTrackingF<Integer> tracker = new InvocationTrackingF<>(1);
20+
Eval<Integer> eval = Eval.later(tracker);
21+
22+
Assert.assertEquals(tracker.getInvocationCounter(), 0);
23+
Assert.assertEquals(eval.value().intValue(), 1);
24+
Assert.assertEquals(tracker.getInvocationCounter(), 1);
25+
eval.value();
26+
Assert.assertEquals(tracker.getInvocationCounter(), 1);
27+
}
28+
29+
@Test
30+
public void testAlways() {
31+
InvocationTrackingF<Integer> tracker = new InvocationTrackingF<>(1);
32+
Eval<Integer> eval = Eval.always(tracker);
33+
34+
Assert.assertEquals(tracker.getInvocationCounter(), 0);
35+
Assert.assertEquals(eval.value().intValue(), 1);
36+
Assert.assertEquals(tracker.getInvocationCounter(), 1);
37+
eval.value();
38+
eval.value();
39+
Assert.assertEquals(tracker.getInvocationCounter(), 3);
40+
}
41+
42+
@Test
43+
public void testDefer() {
44+
// Make sure that a recursive computation is actually stack-safe.
45+
Assert.assertEquals(even(200000).value(), "done");
46+
}
47+
48+
private static Eval<String> even(int n) {
49+
return Eval.now(n <= 0).flatMap(b -> b ? Eval.now("done") : Eval.defer(() -> odd(n - 1)));
50+
}
51+
52+
private static Eval<String> odd(int n) {
53+
return Eval.defer(() -> even(n - 1));
54+
}
55+
56+
private static class InvocationTrackingF<A> implements F0<A> {
57+
58+
private final A value;
59+
private int invocationCounter;
60+
61+
public InvocationTrackingF(A value) {
62+
this.value = value;
63+
this.invocationCounter = 0;
64+
}
65+
66+
@Override
67+
public A f() {
68+
invocationCounter++;
69+
return value;
70+
}
71+
72+
public int getInvocationCounter() {
73+
return invocationCounter;
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)