Skip to content

Commit a88cd97

Browse files
committed
Bracket for producing a value from an IO that can be cleaned up
1 parent e8a0352 commit a88cd97

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
4747
- `IO#safe`, mapping an `IO<A>` to an `IO<Either<Throwable, A>>` that will never throw
4848
- `IO#ensuring`, like `finally` semantics for `IO`s
4949
- `IO#throwing`, for producing an `IO<A>` that will throw a given `Throwable` when executed
50+
- `Bracket`, for bracketing an `IO` operation with a mapping operation and a cleanup operation
5051

5152
## [3.3.0] - 2019-02-18
5253
### Added
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.jnape.palatable.lambda.functions.builtin.fn3;
2+
3+
import com.jnape.palatable.lambda.functions.Fn1;
4+
import com.jnape.palatable.lambda.functions.Fn2;
5+
import com.jnape.palatable.lambda.functions.Fn3;
6+
import com.jnape.palatable.lambda.io.IO;
7+
import com.jnape.palatable.lambda.monad.Monad;
8+
9+
/**
10+
* Given an {@link IO} that yields some type <code>A</code>, a cleanup operation to run if a value of that type could be
11+
* provisioned, and a kleisli arrow from that type to a new {@link IO} of type <code>B</code>, produce an
12+
* <code>{@link IO}&lt;B&gt;</code> that, when run, will provision the <code>A</code>,
13+
* {@link Monad#flatMap(Fn1) flatMap} it to <code>B</code>, and clean up the original value if it was produced in the
14+
* first place.
15+
*
16+
* @param <A> the initial value to map and clean up
17+
* @param <B> the resulting type
18+
*/
19+
public final class Bracket<A, B> implements
20+
Fn3<IO<A>, Fn1<? super A, ? extends IO<?>>, Fn1<? super A, ? extends IO<B>>, IO<B>> {
21+
22+
private static final Bracket<?, ?> INSTANCE = new Bracket<>();
23+
24+
private Bracket() {
25+
}
26+
27+
@Override
28+
public IO<B> checkedApply(IO<A> io, Fn1<? super A, ? extends IO<?>> cleanupIO,
29+
Fn1<? super A, ? extends IO<B>> bodyIO) throws Throwable {
30+
return io.flatMap(a -> bodyIO.apply(a).ensuring(cleanupIO.apply(a)));
31+
}
32+
33+
@SuppressWarnings("unchecked")
34+
public static <A, B> Bracket<A, B> bracket() {
35+
return (Bracket<A, B>) INSTANCE;
36+
}
37+
38+
public static <A, B> Fn2<Fn1<? super A, ? extends IO<?>>, Fn1<? super A, ? extends IO<B>>, IO<B>> bracket(
39+
IO<A> io) {
40+
return Bracket.<A, B>bracket().apply(io);
41+
}
42+
43+
public static <A, B> Fn1<Fn1<? super A, ? extends IO<B>>, IO<B>> bracket(
44+
IO<A> io, Fn1<? super A, ? extends IO<?>> cleanupIO) {
45+
return Bracket.<A, B>bracket(io).apply(cleanupIO);
46+
}
47+
48+
public static <A, B> IO<B> bracket(IO<A> io, Fn1<? super A, ? extends IO<?>> cleanupIO,
49+
Fn1<? super A, ? extends IO<B>> bodyIO) {
50+
return Bracket.<A, B>bracket(io, cleanupIO).apply(bodyIO);
51+
}
52+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.jnape.palatable.lambda.functions.builtin.fn3;
2+
3+
import com.jnape.palatable.lambda.io.IO;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
import static com.jnape.palatable.lambda.functions.builtin.fn3.Bracket.bracket;
10+
import static com.jnape.palatable.lambda.io.IO.io;
11+
import static org.junit.Assert.assertArrayEquals;
12+
import static org.junit.Assert.assertEquals;
13+
import static org.junit.Assert.fail;
14+
15+
public class BracketTest {
16+
17+
private AtomicInteger count;
18+
19+
@Before
20+
public void setUp() {
21+
count = new AtomicInteger(0);
22+
}
23+
24+
@Test
25+
public void cleanupHappyPath() {
26+
IO<Integer> hashIO = bracket(io(() -> count), c -> io(c::incrementAndGet), c -> io(c::hashCode));
27+
28+
assertEquals(0, count.get());
29+
assertEquals((Integer) count.hashCode(), hashIO.unsafePerformIO());
30+
assertEquals(1, count.get());
31+
}
32+
33+
@Test
34+
public void cleanupSadPath() {
35+
IllegalStateException thrown = new IllegalStateException("kaboom");
36+
IO<Integer> hashIO = bracket(io(count), c -> io(c::incrementAndGet), c -> io(() -> {throw thrown;}));
37+
38+
try {
39+
hashIO.unsafePerformIO();
40+
fail("Expected exception to be raised");
41+
} catch (IllegalStateException actual) {
42+
assertEquals(thrown, actual);
43+
assertEquals(1, count.get());
44+
}
45+
}
46+
47+
@Test
48+
public void cleanupOnlyRunsIfInitialIORuns() {
49+
IllegalStateException thrown = new IllegalStateException("kaboom");
50+
IO<Integer> hashIO = bracket(io(() -> {throw thrown;}),
51+
__ -> io(count::incrementAndGet),
52+
__ -> io(count::incrementAndGet));
53+
try {
54+
hashIO.unsafePerformIO();
55+
fail("Expected exception to be raised");
56+
} catch (IllegalStateException actual) {
57+
assertEquals(thrown, actual);
58+
assertEquals(0, count.get());
59+
}
60+
}
61+
62+
@Test
63+
public void errorsInCleanupAreAddedToBodyErrors() {
64+
IllegalStateException bodyError = new IllegalStateException("kaboom");
65+
IllegalStateException cleanupError = new IllegalStateException("KABOOM");
66+
IO<Integer> hashIO = bracket(io(count),
67+
c -> io(() -> {throw cleanupError;}),
68+
c -> io(() -> {throw bodyError;}));
69+
try {
70+
hashIO.unsafePerformIO();
71+
fail("Expected exception to be raised");
72+
} catch (IllegalStateException actual) {
73+
assertEquals(bodyError, actual);
74+
assertArrayEquals(new Throwable[]{cleanupError}, actual.getSuppressed());
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)