Skip to content

Commit 962a3c4

Browse files
committed
Try#withResources
1 parent ddae951 commit 962a3c4

File tree

3 files changed

+141
-0
lines changed

3 files changed

+141
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
1919
- `Fn1#thunk`, producing an `Fn0`
2020
- `Absent`, a monoid over `Maybe` that is absence biased
2121
- `RateLimit`, a function that iterates elements from an `Iterable` according to some rate limit
22+
- `Try#withResources`, `Try`'s expression analog to Java 7's try-with-resources statement
2223

2324
### Changed
2425
- `Tuple2-8` now implement `Product2-8`

src/main/java/com/jnape/palatable/lambda/adt/Try.java

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

33
import com.jnape.palatable.lambda.adt.coproduct.CoProduct2;
4+
import com.jnape.palatable.lambda.functions.specialized.checked.CheckedFn1;
45
import com.jnape.palatable.lambda.functions.specialized.checked.CheckedRunnable;
56
import com.jnape.palatable.lambda.functions.specialized.checked.CheckedSupplier;
67
import com.jnape.palatable.lambda.functor.Applicative;
@@ -15,6 +16,7 @@
1516
import static com.jnape.palatable.lambda.adt.Unit.UNIT;
1617
import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly;
1718
import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id;
19+
import static com.jnape.palatable.lambda.functions.builtin.fn1.Upcast.upcast;
1820
import static com.jnape.palatable.lambda.functions.builtin.fn2.Peek2.peek2;
1921

2022
/**
@@ -246,6 +248,85 @@ public static <T extends Throwable> Try<T, Unit> trying(CheckedRunnable<T> runna
246248
});
247249
}
248250

251+
/**
252+
* Given a <code>{@link CheckedSupplier}&lt;{@link AutoCloseable}&gt;</code> <code>aSupplier</code> and a
253+
* {@link Function} <code>fn</code>, apply <code>fn</code> to the result of <code>aSupplier</code>, ensuring
254+
* that the result has its {@link AutoCloseable#close() close} method invoked, regardless of the outcome.
255+
* <p>
256+
* If the resource creation process throws, the function body throws, or the
257+
* {@link AutoCloseable#close() close method} throws, the result is a failure. If both the function body and the
258+
* {@link AutoCloseable#close() close method} throw, the result is a failure over the function body
259+
* {@link Throwable} with the {@link AutoCloseable#close() close method} {@link Throwable} added as a
260+
* {@link Throwable#addSuppressed(Throwable) suppressed} {@link Throwable}. If only the
261+
* {@link AutoCloseable#close() close method} throws, the result is a failure over that {@link Throwable}.
262+
* <p>
263+
* Note that <code>withResources</code> calls can be nested, in which case all of the above specified exception
264+
* handling applies, where closing the previously created resource is considered part of the body of the next
265+
* <code>withResources</code> calls, and {@link Throwable Throwables} are considered suppressed in the same manner.
266+
* Additionally, {@link AutoCloseable#close() close methods} are invoked in the inverse order of resource creation.
267+
* <p>
268+
* This is {@link Try}'s equivalent of
269+
* <a href="https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html" target="_top">
270+
* try-with-resources</a>, introduced in Java 7.
271+
*
272+
* @param aSupplier the resource supplier
273+
* @param fn the function body
274+
* @param <A> the resource type
275+
* @param <B> the function return type
276+
* @return a {@link Try} representing the result of the function's application to the resource
277+
*/
278+
public static <A extends AutoCloseable, B> Try<Exception, B> withResources(
279+
CheckedSupplier<? extends Exception, A> aSupplier,
280+
CheckedFn1<? extends Exception, ? super A, ? extends Try<? extends Exception, ? extends B>> fn) {
281+
return trying(() -> {
282+
try (A resource = aSupplier.get()) {
283+
return fn.apply(resource).<Exception, B>biMap(upcast(), upcast());
284+
}
285+
}).flatMap(id());
286+
}
287+
288+
/**
289+
* Convenience overload of {@link Try#withResources(CheckedSupplier, CheckedFn1) withResources} that cascades
290+
* dependent resource creation via nested calls.
291+
*
292+
* @param aSupplier the first resource supplier
293+
* @param bFn the dependent resource function
294+
* @param fn the function body
295+
* @param <A> the first resource type
296+
* @param <B> the second resource type
297+
* @param <C> the function return type
298+
* @return a {@link Try} representing the result of the function's application to the dependent resource
299+
*/
300+
public static <A extends AutoCloseable, B extends AutoCloseable, C> Try<Exception, C> withResources(
301+
CheckedSupplier<? extends Exception, ? extends A> aSupplier,
302+
CheckedFn1<? extends Exception, ? super A, ? extends B> bFn,
303+
CheckedFn1<? extends Exception, ? super B, ? extends Try<? extends Exception, ? extends C>> fn) {
304+
return withResources(aSupplier, a -> withResources(() -> bFn.apply(a), fn::apply));
305+
}
306+
307+
/**
308+
* Convenience overload of {@link Try#withResources(CheckedSupplier, CheckedFn1, CheckedFn1) withResources} that
309+
* cascades
310+
* two dependent resource creations via nested calls.
311+
*
312+
* @param aSupplier the first resource supplier
313+
* @param bFn the second resource function
314+
* @param cFn the final resource function
315+
* @param fn the function body
316+
* @param <A> the first resource type
317+
* @param <B> the second resource type
318+
* @param <C> the final resource type
319+
* @param <D> the function return type
320+
* @return a {@link Try} representing the result of the function's application to the final dependent resource
321+
*/
322+
public static <A extends AutoCloseable, B extends AutoCloseable, C extends AutoCloseable, D> Try<Exception, D> withResources(
323+
CheckedSupplier<? extends Exception, ? extends A> aSupplier,
324+
CheckedFn1<? extends Exception, ? super A, ? extends B> bFn,
325+
CheckedFn1<? extends Exception, ? super B, ? extends C> cFn,
326+
CheckedFn1<? extends Exception, ? super C, ? extends Try<? extends Exception, ? extends D>> fn) {
327+
return withResources(aSupplier, bFn, b -> withResources(() -> cFn.apply(b), fn::apply));
328+
}
329+
249330
private static final class Failure<T extends Throwable, A> extends Try<T, A> {
250331
private final T t;
251332

src/test/java/com/jnape/palatable/lambda/adt/TryTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
import testsupport.traits.MonadLaws;
1313
import testsupport.traits.TraversableLaws;
1414

15+
import java.io.IOException;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
import java.util.concurrent.atomic.AtomicBoolean;
1519
import java.util.concurrent.atomic.AtomicInteger;
1620

1721
import static com.jnape.palatable.lambda.adt.Either.left;
@@ -22,11 +26,15 @@
2226
import static com.jnape.palatable.lambda.adt.Try.success;
2327
import static com.jnape.palatable.lambda.adt.Try.trying;
2428
import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly;
29+
import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id;
2530
import static com.jnape.palatable.traitor.framework.Subjects.subjects;
31+
import static java.util.Arrays.asList;
2632
import static org.hamcrest.CoreMatchers.equalTo;
2733
import static org.hamcrest.CoreMatchers.instanceOf;
34+
import static org.junit.Assert.assertArrayEquals;
2835
import static org.junit.Assert.assertEquals;
2936
import static org.junit.Assert.assertThat;
37+
import static org.junit.Assert.assertTrue;
3038
import static testsupport.matchers.LeftMatcher.isLeftThat;
3139

3240
@RunWith(Traits.class)
@@ -150,4 +158,55 @@ public void tryingCatchesAnyThrowableThrownDuringEvaluation() {
150158

151159
assertEquals(success("foo"), trying(() -> "foo"));
152160
}
161+
162+
@Test
163+
public void withResourcesCleansUpAutoCloseableInSuccessCase() {
164+
AtomicBoolean closed = new AtomicBoolean(false);
165+
assertEquals(Try.success(1), Try.withResources(() -> () -> closed.set(true), resource -> success(1)));
166+
assertTrue(closed.get());
167+
}
168+
169+
@Test
170+
public void withResourcesCleansUpAutoCloseableInFailureCase() {
171+
AtomicBoolean closed = new AtomicBoolean(false);
172+
RuntimeException exception = new RuntimeException();
173+
assertEquals(Try.failure(exception), Try.withResources(() -> () -> closed.set(true),
174+
resource -> { throw exception; }));
175+
assertTrue(closed.get());
176+
}
177+
178+
@Test
179+
public void withResourcesExposesResourceCreationFailure() {
180+
IOException ioException = new IOException();
181+
assertEquals(Try.failure(ioException), Try.withResources(() -> { throw ioException; }, resource -> success(1)));
182+
}
183+
184+
@Test
185+
public void withResourcesExposesResourceCloseFailure() {
186+
IOException ioException = new IOException();
187+
assertEquals(Try.failure(ioException), Try.withResources(() -> () -> { throw ioException; },
188+
resource -> success(1)));
189+
}
190+
191+
@Test
192+
public void withResourcesPreservesSuppressedExceptionThrownDuringClose() {
193+
RuntimeException rootException = new RuntimeException();
194+
IOException nestedIOException = new IOException();
195+
Try<Exception, Exception> failure = Try.withResources(() -> () -> { throw nestedIOException; },
196+
resource -> { throw rootException; });
197+
Exception thrown = failure.recover(id());
198+
199+
assertEquals(thrown, rootException);
200+
assertArrayEquals(new Throwable[]{nestedIOException}, thrown.getSuppressed());
201+
}
202+
203+
@Test
204+
public void cascadingWithResourcesClosesInInverseOrder() {
205+
List<String> closeMessages = new ArrayList<>();
206+
assertEquals(success(1), Try.withResources(() -> (AutoCloseable) () -> closeMessages.add("close a"),
207+
a -> () -> closeMessages.add("close b"),
208+
b -> () -> closeMessages.add("close c"),
209+
c -> success(1)));
210+
assertEquals(asList("close c", "close b", "close a"), closeMessages);
211+
}
153212
}

0 commit comments

Comments
 (0)