Skip to content

Commit cfa6317

Browse files
committed
IO#exceptionallyIO
1 parent 581cea5 commit cfa6317

File tree

3 files changed

+63
-35
lines changed

3 files changed

+63
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
2121
- `IO#monitorSync`, for wrapping an `IO` in a `synchronized` block on a given lock object
2222
- `IO#pin`, for pinning an `IO` to an `Executor` without yet executing it
2323
- `IO#fuse`, for fusing the fork opportunities of a given `IO` into a single linearized `IO`
24+
- `IO#exceptionallyIO`, like `exceptionally` but recover inside another `IO`
2425

2526
## [4.0.0] - 2019-05-20
2627
### Changed

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

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

33
import com.jnape.palatable.lambda.adt.Either;
4+
import com.jnape.palatable.lambda.adt.Try;
45
import com.jnape.palatable.lambda.adt.Unit;
56
import com.jnape.palatable.lambda.adt.choice.Choice2;
67
import com.jnape.palatable.lambda.functions.Fn0;
@@ -15,6 +16,7 @@
1516
import java.util.concurrent.CompletableFuture;
1617
import java.util.concurrent.Executor;
1718

19+
import static com.jnape.palatable.lambda.adt.Try.failure;
1820
import static com.jnape.palatable.lambda.adt.Try.trying;
1921
import static com.jnape.palatable.lambda.adt.Unit.UNIT;
2022
import static com.jnape.palatable.lambda.adt.choice.Choice2.a;
@@ -81,43 +83,48 @@ public final CompletableFuture<A> unsafePerformAsyncIO() {
8183
* @return the guarded {@link IO}
8284
*/
8385
public final IO<A> exceptionally(Fn1<? super Throwable, ? extends A> recoveryFn) {
86+
return exceptionallyIO(t -> io(recoveryFn.apply(t)));
87+
}
88+
89+
/**
90+
* Like {@link IO#exceptionally(Fn1) exceptionally}, but recover the {@link Throwable} via another {@link IO}
91+
* operation. If both {@link IO IOs} throw, the "cleanup" {@link IO IO's} {@link Throwable} is
92+
* {@link Throwable#addSuppressed(Throwable) suppressed} by this {@link IO IO's} {@link Throwable}.
93+
*
94+
* @param recoveryFn the recovery function
95+
* @return the guarded {@link IO}
96+
*/
97+
public final IO<A> exceptionallyIO(Fn1<? super Throwable, ? extends IO<A>> recoveryFn) {
8498
return new IO<A>() {
8599
@Override
86100
public A unsafePerformIO() {
87-
return trying(IO.this::unsafePerformIO).recover(recoveryFn);
101+
return trying(IO.this::unsafePerformIO)
102+
.recover(t -> trying(recoveryFn.apply(t)::unsafePerformIO)
103+
.fmap(Try::success)
104+
.recover(t2 -> {
105+
t.addSuppressed(t2);
106+
return failure(t);
107+
})
108+
.orThrow());
88109
}
89110

90111
@Override
91112
public CompletableFuture<A> unsafePerformAsyncIO(Executor executor) {
92-
return IO.this.unsafePerformAsyncIO(executor).exceptionally(recoveryFn::apply);
113+
return IO.this.unsafePerformAsyncIO(executor)
114+
.thenApply(CompletableFuture::completedFuture)
115+
.exceptionally(t -> recoveryFn.apply(t).unsafePerformAsyncIO(executor)
116+
.thenApply(CompletableFuture::completedFuture)
117+
.exceptionally(t2 -> {
118+
t.addSuppressed(t2);
119+
return new CompletableFuture<A>() {{
120+
completeExceptionally(t);
121+
}};
122+
}).thenCompose(f -> f))
123+
.thenCompose(f -> f);
93124
}
94125
};
95126
}
96127

97-
// /**
98-
// * Given a function from any {@link Throwable} to the result type <code>A</code>, if this {@link IO} successfully
99-
// * yields a result, return it; otherwise, map the {@link Throwable} to the result type and return that.
100-
// *
101-
// * @param recoveryFn the recovery function
102-
// * @return the guarded {@link IO}
103-
// */
104-
// public final IO<A> exceptionallyIO(Fn1<? super Throwable, ? extends IO<A>> recoveryFn) {
105-
// return new IO<A>() {
106-
// @Override
107-
// public A unsafePerformIO() {
108-
// return trying(IO.this::unsafePerformIO).recover(t -> recoveryFn.apply(t).unsafePerformIO())
109-
// }
110-
//
111-
// @Override
112-
// public CompletableFuture<A> unsafePerformAsyncIO(Executor executor) {
113-
//// IO.this.unsafePerformAsyncIO(executor)
114-
//// .thenCombine()
115-
//// .exceptionally(t -> recoveryFn.apply(t).unsafePerformAsyncIO(executor))
116-
// }
117-
// };
118-
// }
119-
120-
121128
/**
122129
* Return an {@link IO} that will run <code>ensureIO</code> strictly after running this {@link IO} regardless of
123130
* whether this {@link IO} terminates normally, analogous to a finally block.

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,12 @@ public void delegatesToExternallyManagedFuture() {
120120
}
121121

122122
@Test
123-
public void exceptionallyRecoversThrowableToResult() {
124-
IO<String> io = io(() -> { throw new UnsupportedOperationException("foo"); });
123+
public void exceptionally() {
124+
Executor executor = newFixedThreadPool(2);
125+
IO<String> io = io(() -> { throw new UnsupportedOperationException("foo"); });
125126
assertEquals("foo", io.exceptionally(Throwable::getMessage).unsafePerformIO());
127+
assertEquals("foo", io.exceptionally(e -> e.getCause().getMessage()).unsafePerformAsyncIO().join());
128+
assertEquals("foo", io.exceptionally(e -> e.getCause().getMessage()).unsafePerformAsyncIO(executor).join());
126129

127130
IO<String> externallyManaged = externallyManaged(() -> new CompletableFuture<String>() {{
128131
completeExceptionally(new UnsupportedOperationException("foo"));
@@ -131,19 +134,36 @@ public void exceptionallyRecoversThrowableToResult() {
131134
}
132135

133136
@Test
134-
public void exceptionallyRescuesFutures() {
135-
ExecutorService executor = newFixedThreadPool(2);
136-
137-
IO<String> io = io(() -> { throw new UnsupportedOperationException("foo"); });
138-
assertEquals("foo", io.exceptionally(e -> e.getCause().getMessage()).unsafePerformAsyncIO().join());
139-
assertEquals("foo", io.exceptionally(e -> e.getCause().getMessage()).unsafePerformAsyncIO(executor).join());
137+
public void exceptionallyIO() {
138+
Executor executor = newFixedThreadPool(2);
139+
IO<String> io = IO.throwing(new UnsupportedOperationException("foo"));
140+
assertEquals("foo", io.exceptionallyIO(t -> io(t::getMessage)).unsafePerformIO());
141+
assertEquals("foo",
142+
io.exceptionallyIO(e -> io(() -> e.getCause().getMessage())).unsafePerformAsyncIO().join());
143+
assertEquals("foo",
144+
io.exceptionallyIO(e -> io(() -> e.getCause().getMessage()))
145+
.unsafePerformAsyncIO(executor).join());
140146

141147
IO<String> externallyManaged = externallyManaged(() -> new CompletableFuture<String>() {{
142148
completeExceptionally(new UnsupportedOperationException("foo"));
143-
}}).exceptionally(e -> e.getCause().getMessage());
149+
}}).exceptionallyIO(e -> io(() -> e.getCause().getMessage()));
144150
assertEquals("foo", externallyManaged.unsafePerformIO());
145151
}
146152

153+
@Test
154+
public void exceptionallyIOSuppressesSecondaryThrowable() {
155+
Throwable foo = new UnsupportedOperationException("foo");
156+
Throwable bar = new UnsupportedOperationException("bar");
157+
158+
try {
159+
IO.throwing(foo).exceptionallyIO(t -> IO.throwing(bar)).unsafePerformIO();
160+
fail("Expected exception to have been thrown, but wasn't.");
161+
} catch (UnsupportedOperationException expected) {
162+
assertEquals(expected, foo);
163+
assertArrayEquals(new Throwable[]{bar}, expected.getSuppressed());
164+
}
165+
}
166+
147167
@Test
148168
public void safe() {
149169
assertEquals(right(1), io(() -> 1).safe().unsafePerformIO());

0 commit comments

Comments
 (0)