Skip to content

Commit 05356aa

Browse files
committed
Adding Lazy, a monad representing a lazy computation
1 parent 08ebc8e commit 05356aa

5 files changed

Lines changed: 227 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ factory methods should continue to work (by simply targeting `Supplier` now inst
1010
might need to be reworked, and subtyping is obviously no longer supported.
1111
- `IO` is now stack-safe, regardless of whether the composition nests linearly or recursively
1212

13+
### Added
14+
- `Lazy`, a monad supporting stack-safe lazy evaluation
15+
1316
## [3.3.0] - 2019-02-18
1417
### Added
1518
- `MergeMaps`, a `Monoid` on `Map` formed by `Map#merge`
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.jnape.palatable.lambda.functor.builtin;
2+
3+
import com.jnape.palatable.lambda.adt.hlist.Tuple2;
4+
import com.jnape.palatable.lambda.functions.Fn0;
5+
import com.jnape.palatable.lambda.functor.Applicative;
6+
import com.jnape.palatable.lambda.monad.Monad;
7+
8+
import java.util.LinkedList;
9+
import java.util.function.Function;
10+
import java.util.function.Supplier;
11+
12+
import static com.jnape.palatable.lambda.adt.hlist.HList.tuple;
13+
import static com.jnape.palatable.lambda.functions.Fn0.fn0;
14+
import static com.jnape.palatable.lambda.functions.builtin.fn2.Into.into;
15+
import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.recurse;
16+
import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.terminate;
17+
import static com.jnape.palatable.lambda.functions.recursion.Trampoline.trampoline;
18+
19+
/**
20+
* A {@link Monad} representing a lazily-computed value. Stack-safe.
21+
*
22+
* @param <A> the value type
23+
*/
24+
public abstract class Lazy<A> implements Monad<A, Lazy<?>> {
25+
26+
private Lazy() {
27+
}
28+
29+
/**
30+
* Returns the value represented by this lazy computation.
31+
*
32+
* @return the value
33+
*/
34+
public abstract A value();
35+
36+
/**
37+
* {@inheritDoc}
38+
*/
39+
@Override
40+
public <B> Lazy<B> flatMap(Function<? super A, ? extends Monad<B, Lazy<?>>> f) {
41+
@SuppressWarnings("unchecked") Lazy<Object> source = (Lazy<Object>) this;
42+
@SuppressWarnings({"unchecked", "RedundantCast"})
43+
Function<Object, Lazy<Object>> flatMap = (Function<Object, Lazy<Object>>) (Object) f;
44+
return new Compose<>(source, flatMap);
45+
}
46+
47+
/**
48+
* {@inheritDoc}
49+
*/
50+
@Override
51+
public <B> Lazy<B> pure(B b) {
52+
return lazy(b);
53+
}
54+
55+
/**
56+
* {@inheritDoc}
57+
*/
58+
@Override
59+
public <B> Lazy<B> fmap(Function<? super A, ? extends B> fn) {
60+
return Monad.super.<B>fmap(fn).coerce();
61+
}
62+
63+
/**
64+
* {@inheritDoc}
65+
*/
66+
@Override
67+
public <B> Lazy<B> zip(Applicative<Function<? super A, ? extends B>, Lazy<?>> appFn) {
68+
return Monad.super.zip(appFn).coerce();
69+
}
70+
71+
/**
72+
* {@inheritDoc}
73+
*/
74+
@Override
75+
public <B> Lazy<B> discardL(Applicative<B, Lazy<?>> appB) {
76+
return Monad.super.discardL(appB).coerce();
77+
}
78+
79+
/**
80+
* {@inheritDoc}
81+
*/
82+
@Override
83+
public <B> Lazy<A> discardR(Applicative<B, Lazy<?>> appB) {
84+
return Monad.super.discardR(appB).coerce();
85+
}
86+
87+
/**
88+
* Lift a pure value into a lazy computation.
89+
*
90+
* @param value the value
91+
* @param <A> the value type
92+
* @return the new {@link Lazy}
93+
*/
94+
public static <A> Lazy<A> lazy(A value) {
95+
return lazy(() -> value);
96+
}
97+
98+
/**
99+
* Wrap a computation in a lazy computation.
100+
*
101+
* @param supplier the computation
102+
* @param <A> the value type
103+
* @return the new {@link Lazy}
104+
*/
105+
public static <A> Lazy<A> lazy(Supplier<A> supplier) {
106+
return new Val<>(fn0(supplier));
107+
}
108+
109+
private static final class Val<A> extends Lazy<A> {
110+
private final Fn0<A> fn0;
111+
112+
private Val(Fn0<A> fn0) {
113+
this.fn0 = fn0;
114+
}
115+
116+
@Override
117+
public A value() {
118+
return fn0.apply();
119+
}
120+
}
121+
122+
private static final class Compose<A> extends Lazy<A> {
123+
private final Lazy<Object> source;
124+
private final Function<Object, Lazy<Object>> flatMap;
125+
126+
private Compose(Lazy<Object> source,
127+
Function<Object, Lazy<Object>> flatMap) {
128+
this.source = source;
129+
this.flatMap = flatMap;
130+
}
131+
132+
@Override
133+
public A value() {
134+
@SuppressWarnings("unchecked") Tuple2<Lazy<Object>, LinkedList<Function<Object, Lazy<Object>>>> tuple =
135+
tuple((Lazy<Object>) this, new LinkedList<>());
136+
@SuppressWarnings("unchecked")
137+
A a = (A) trampoline(into((source, flatMaps) -> {
138+
if (source instanceof Compose<?>) {
139+
Compose<?> nested = (Compose<?>) source;
140+
flatMaps.push(nested.flatMap);
141+
return recurse(tuple(nested.source, flatMaps));
142+
}
143+
144+
if (flatMaps.isEmpty())
145+
return terminate(source.value());
146+
147+
return recurse(tuple(flatMaps.pop().apply(source.value()), flatMaps));
148+
}), tuple);
149+
150+
return a;
151+
}
152+
}
153+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.jnape.palatable.lambda.functor.builtin;
2+
3+
import org.junit.Test;
4+
5+
import java.util.concurrent.atomic.AtomicBoolean;
6+
import java.util.function.Function;
7+
8+
import static com.jnape.palatable.lambda.functions.builtin.fn3.Times.times;
9+
import static com.jnape.palatable.lambda.functor.builtin.Lazy.lazy;
10+
import static org.junit.Assert.assertEquals;
11+
import static org.junit.Assert.assertFalse;
12+
import static org.junit.Assert.assertTrue;
13+
import static testsupport.Constants.STACK_EXPLODING_NUMBER;
14+
15+
public class LazyTest {
16+
17+
@Test
18+
public void valueExtraction() {
19+
assertEquals("foo", lazy("foo").value());
20+
assertEquals("foo", lazy(() -> "foo").value());
21+
}
22+
23+
@Test
24+
public void lazyEvaluation() {
25+
AtomicBoolean invoked = new AtomicBoolean(false);
26+
Lazy<Integer> lazy = lazy(0).flatMap(x -> {
27+
invoked.set(true);
28+
return lazy(x + 1);
29+
});
30+
assertFalse(invoked.get());
31+
assertEquals((Integer) 1, lazy.value());
32+
assertTrue(invoked.get());
33+
}
34+
35+
@Test
36+
public void linearStackSafety() {
37+
assertEquals(STACK_EXPLODING_NUMBER,
38+
times(STACK_EXPLODING_NUMBER, f -> f.fmap(x -> x + 1), lazy(0)).value());
39+
}
40+
41+
@Test
42+
public void recursiveStackSafety() {
43+
assertEquals(STACK_EXPLODING_NUMBER,
44+
new Function<Lazy<Integer>, Lazy<Integer>>() {
45+
@Override
46+
public Lazy<Integer> apply(Lazy<Integer> lazy) {
47+
return lazy.flatMap(x -> x < STACK_EXPLODING_NUMBER ? apply(lazy(x + 1)) : lazy(x));
48+
}
49+
}.apply(lazy(0))
50+
.value());
51+
}
52+
}

src/test/java/com/jnape/palatable/lambda/io/IOTest.java

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.jnape.palatable.lambda.adt.hlist.Tuple2;
44
import com.jnape.palatable.lambda.functions.Fn1;
5-
import com.jnape.palatable.lambda.io.IO;
65
import com.jnape.palatable.traitor.annotations.TestTraits;
76
import com.jnape.palatable.traitor.runners.Traits;
87
import org.junit.Test;
@@ -18,21 +17,20 @@
1817
import java.util.function.Function;
1918

2019
import static com.jnape.palatable.lambda.adt.Unit.UNIT;
21-
import static com.jnape.palatable.lambda.io.IO.externallyManaged;
22-
import static com.jnape.palatable.lambda.io.IO.io;
2320
import static com.jnape.palatable.lambda.functions.builtin.fn2.Tupler2.tupler;
2421
import static com.jnape.palatable.lambda.functions.builtin.fn3.Times.times;
2522
import static com.jnape.palatable.lambda.functions.specialized.checked.CheckedSupplier.checked;
23+
import static com.jnape.palatable.lambda.io.IO.externallyManaged;
24+
import static com.jnape.palatable.lambda.io.IO.io;
2625
import static java.util.concurrent.CompletableFuture.completedFuture;
2726
import static java.util.concurrent.Executors.newFixedThreadPool;
2827
import static java.util.concurrent.ForkJoinPool.commonPool;
2928
import static org.junit.Assert.assertEquals;
29+
import static testsupport.Constants.STACK_EXPLODING_NUMBER;
3030

3131
@RunWith(Traits.class)
3232
public class IOTest {
3333

34-
private static final int STACK_EXPLODING_NUMBER = 50_000;
35-
3634
@TestTraits({FunctorLaws.class, ApplicativeLaws.class, MonadLaws.class})
3735
public EqualityAwareIO<Integer> testSubject() {
3836
return new EqualityAwareIO<>(io(1));
@@ -125,23 +123,23 @@ public void exceptionallyRescuesFutures() {
125123

126124
@Test
127125
public void linearSyncStackSafety() {
128-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
126+
assertEquals(STACK_EXPLODING_NUMBER,
129127
times(STACK_EXPLODING_NUMBER, f -> f.fmap(x -> x + 1), io(0)).unsafePerformIO());
130-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
128+
assertEquals(STACK_EXPLODING_NUMBER,
131129
times(STACK_EXPLODING_NUMBER, f -> f.zip(f.pure(x -> x + 1)), io(0)).unsafePerformIO());
132130
assertEquals((Integer) 0,
133131
times(STACK_EXPLODING_NUMBER, f -> f.pure(0).discardR(f), io(0)).unsafePerformIO());
134132
assertEquals((Integer) 1,
135133
times(STACK_EXPLODING_NUMBER, f -> f.pure(1).discardR(f), io(0)).unsafePerformIO());
136134
assertEquals((Integer) 0,
137135
times(STACK_EXPLODING_NUMBER, f -> f.pure(1).discardL(f), io(0)).unsafePerformIO());
138-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
136+
assertEquals(STACK_EXPLODING_NUMBER,
139137
times(STACK_EXPLODING_NUMBER, f -> f.flatMap(x -> f.pure(x + 1)), io(0)).unsafePerformIO());
140138
}
141139

142140
@Test
143141
public void recursiveSyncFlatMapStackSafety() {
144-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
142+
assertEquals(STACK_EXPLODING_NUMBER,
145143
new Fn1<IO<Integer>, IO<Integer>>() {
146144
@Override
147145
public IO<Integer> apply(IO<Integer> a) {
@@ -153,9 +151,9 @@ public IO<Integer> apply(IO<Integer> a) {
153151

154152
@Test
155153
public void linearAsyncStackSafety() {
156-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
154+
assertEquals(STACK_EXPLODING_NUMBER,
157155
times(STACK_EXPLODING_NUMBER, f -> f.fmap(x -> x + 1), io(0)).unsafePerformAsyncIO().join());
158-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
156+
assertEquals(STACK_EXPLODING_NUMBER,
159157
times(STACK_EXPLODING_NUMBER, f -> f.zip(f.pure(x -> x + 1)), io(0)).unsafePerformAsyncIO()
160158
.join());
161159
assertEquals((Integer) 0,
@@ -164,14 +162,14 @@ public void linearAsyncStackSafety() {
164162
times(STACK_EXPLODING_NUMBER, f -> f.pure(1).discardR(f), io(0)).unsafePerformAsyncIO().join());
165163
assertEquals((Integer) 0,
166164
times(STACK_EXPLODING_NUMBER, f -> f.pure(1).discardL(f), io(0)).unsafePerformAsyncIO().join());
167-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
165+
assertEquals(STACK_EXPLODING_NUMBER,
168166
times(STACK_EXPLODING_NUMBER, f -> f.flatMap(x -> f.pure(x + 1)), io(0)).unsafePerformAsyncIO()
169167
.join());
170168
}
171169

172170
@Test
173171
public void recursiveAsyncFlatMapStackSafety() {
174-
assertEquals((Integer) STACK_EXPLODING_NUMBER,
172+
assertEquals(STACK_EXPLODING_NUMBER,
175173
new Fn1<IO<Integer>, IO<Integer>>() {
176174
@Override
177175
public IO<Integer> apply(IO<Integer> a) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package testsupport;
2+
3+
public final class Constants {
4+
public static final Integer STACK_EXPLODING_NUMBER = 50_000;
5+
6+
private Constants() {
7+
}
8+
}

0 commit comments

Comments
 (0)