Skip to content

Commit e8a0352

Browse files
committed
IO#safe, IO#ensuring, and IO#throwing
1 parent d32692c commit e8a0352

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
4444
- `Re` for viewing an `Optic` in one direction reliably
4545
- `Pre` for viewing at most one value from an `Optic` in one direction
4646
- `SideEffect`, for representing side-effects runnable by `IO`
47+
- `IO#safe`, mapping an `IO<A>` to an `IO<Either<Throwable, A>>` that will never throw
48+
- `IO#ensuring`, like `finally` semantics for `IO`s
49+
- `IO#throwing`, for producing an `IO<A>` that will throw a given `Throwable` when executed
4750

4851
## [3.3.0] - 2019-02-18
4952
### Added

src/main/java/com/jnape/palatable/lambda/io/IO.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.jnape.palatable.lambda.io;
22

3+
import com.jnape.palatable.lambda.adt.Either;
34
import com.jnape.palatable.lambda.adt.Try;
45
import com.jnape.palatable.lambda.adt.Unit;
56
import com.jnape.palatable.lambda.adt.choice.Choice2;
@@ -19,11 +20,13 @@
1920
import static com.jnape.palatable.lambda.adt.choice.Choice2.a;
2021
import static com.jnape.palatable.lambda.adt.hlist.HList.tuple;
2122
import static com.jnape.palatable.lambda.functions.Fn0.fn0;
23+
import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly;
2224
import static com.jnape.palatable.lambda.functions.builtin.fn2.Into.into;
2325
import static com.jnape.palatable.lambda.functions.builtin.fn3.FoldLeft.foldLeft;
2426
import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.recurse;
2527
import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.terminate;
2628
import static com.jnape.palatable.lambda.functions.recursion.Trampoline.trampoline;
29+
import static com.jnape.palatable.lambda.monad.Monad.join;
2730
import static java.util.concurrent.CompletableFuture.completedFuture;
2831
import static java.util.concurrent.CompletableFuture.supplyAsync;
2932
import static java.util.concurrent.ForkJoinPool.commonPool;
@@ -93,6 +96,32 @@ public CompletableFuture<A> unsafePerformAsyncIO(Executor executor) {
9396
};
9497
}
9598

99+
/**
100+
* Return an {@link IO} that will run <code>ensureIO</code> strictly after running this {@link IO} regardless of
101+
* whether this {@link IO} terminates normally, analogous to a finally block.
102+
*
103+
* @param ensureIO the {@link IO} to ensure runs strictly after this {@link IO}
104+
* @return the combined {@link IO}
105+
*/
106+
public final IO<A> ensuring(IO<?> ensureIO) {
107+
return join(fmap(a -> ensureIO.fmap(constantly(a)))
108+
.exceptionally(t -> join(ensureIO.<IO<A>>fmap(constantly(io(() -> {throw t;})))
109+
.exceptionally(t2 -> io(() -> {
110+
t.addSuppressed(t2);
111+
throw t;
112+
})))));
113+
}
114+
115+
/**
116+
* Return a safe {@link IO} that will never throw by lifting the result of this {@link IO} into {@link Either},
117+
* catching any {@link Throwable} and wrapping it in a {@link Either#left(Object) left}.
118+
*
119+
* @return the safe {@link IO}
120+
*/
121+
public final IO<Either<Throwable, A>> safe() {
122+
return fmap(Either::<Throwable, A>right).exceptionally(Either::left);
123+
}
124+
96125
/**
97126
* {@inheritDoc}
98127
*/
@@ -157,6 +186,17 @@ public final <B> IO<B> flatMap(Fn1<? super A, ? extends Monad<B, IO<?>>> f) {
157186
return new Compose<>(source, Choice2.b(flatMap));
158187
}
159188

189+
/**
190+
* Produce an {@link IO} that throws the given {@link Throwable} when executed.
191+
*
192+
* @param t the {@link Throwable}
193+
* @param <A> any result type
194+
* @return the {@link IO}
195+
*/
196+
public static <A> IO<A> throwing(Throwable t) {
197+
return io(() -> {throw t;});
198+
}
199+
160200
/**
161201
* Static factory method for creating an {@link IO} that just returns <code>a</code> when performed.
162202
*

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@
1313

1414
import java.util.concurrent.CompletableFuture;
1515
import java.util.concurrent.CountDownLatch;
16+
import java.util.concurrent.Executor;
1617
import java.util.concurrent.ExecutorService;
18+
import java.util.concurrent.Executors;
19+
import java.util.concurrent.atomic.AtomicInteger;
1720

21+
import static com.jnape.palatable.lambda.adt.Either.left;
22+
import static com.jnape.palatable.lambda.adt.Either.right;
1823
import static com.jnape.palatable.lambda.adt.Unit.UNIT;
1924
import static com.jnape.palatable.lambda.functions.builtin.fn2.Tupler2.tupler;
2025
import static com.jnape.palatable.lambda.functions.builtin.fn3.Times.times;
@@ -23,7 +28,9 @@
2328
import static java.util.concurrent.CompletableFuture.completedFuture;
2429
import static java.util.concurrent.Executors.newFixedThreadPool;
2530
import static java.util.concurrent.ForkJoinPool.commonPool;
31+
import static org.junit.Assert.assertArrayEquals;
2632
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.fail;
2734
import static testsupport.Constants.STACK_EXPLODING_NUMBER;
2835

2936
@RunWith(Traits.class)
@@ -174,6 +181,66 @@ public IO<Integer> checkedApply(IO<Integer> a) {
174181
return a.flatMap(x -> x < STACK_EXPLODING_NUMBER ? apply(io(x + 1)) : io(x));
175182
}
176183
}.apply(io(0)).unsafePerformAsyncIO().join());
184+
}
185+
186+
@Test
187+
public void safe() {
188+
assertEquals(right(1), io(() -> 1).safe().unsafePerformIO());
189+
IllegalStateException thrown = new IllegalStateException("kaboom");
190+
assertEquals(left(thrown), io(() -> {throw thrown;}).safe().unsafePerformIO());
191+
}
192+
193+
@Test
194+
public void ensuring() {
195+
AtomicInteger counter = new AtomicInteger(0);
196+
IO<Integer> incCounter = io(counter::incrementAndGet);
197+
assertEquals("foo", io(() -> "foo").ensuring(incCounter).unsafePerformIO());
198+
assertEquals(1, counter.get());
199+
200+
IllegalStateException thrown = new IllegalStateException("kaboom");
201+
try {
202+
io(() -> {throw thrown;}).ensuring(incCounter).unsafePerformIO();
203+
fail("Expected exception to have been thrown, but wasn't.");
204+
} catch (IllegalStateException actual) {
205+
assertEquals(thrown, actual);
206+
assertEquals(2, counter.get());
207+
}
208+
}
177209

210+
@Test
211+
public void ensuringRunsStrictlyAfterIO() {
212+
Executor twoThreads = Executors.newFixedThreadPool(2);
213+
AtomicInteger counter = new AtomicInteger(0);
214+
io(() -> {
215+
Thread.sleep(100);
216+
counter.incrementAndGet();
217+
}).ensuring(io(() -> {
218+
if (counter.get() == 0)
219+
fail("Expected to run after initial IO, but ran first");
220+
})).unsafePerformAsyncIO(twoThreads).join();
221+
}
222+
223+
@Test
224+
public void ensuringAttachesThrownExceptionToThrownBodyException() {
225+
IllegalStateException thrownByBody = new IllegalStateException("kaboom");
226+
IllegalStateException thrownByEnsuring = new IllegalStateException("KABOOM");
227+
228+
try {
229+
io(() -> {throw thrownByBody;}).ensuring(io(() -> {throw thrownByEnsuring;})).unsafePerformIO();
230+
fail("Expected exception to have been thrown, but wasn't.");
231+
} catch (IllegalStateException actual) {
232+
assertEquals(thrownByBody, actual);
233+
assertArrayEquals(new Throwable[]{thrownByEnsuring}, actual.getSuppressed());
234+
}
235+
}
236+
237+
@Test
238+
public void throwing() {
239+
IllegalStateException expected = new IllegalStateException("thrown");
240+
try {
241+
IO.throwing(expected).unsafePerformIO();
242+
} catch (IllegalStateException actual) {
243+
assertEquals(expected, actual);
244+
}
178245
}
179246
}

0 commit comments

Comments
 (0)