Skip to content

Commit ae7b7cd

Browse files
refactor: Add Result.fromTry
(May or may not involve tricking the compiler)
1 parent 75e0bb3 commit ae7b7cd

4 files changed

Lines changed: 81 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package net.marcellperger.mathexpr.util;
2+
3+
@FunctionalInterface
4+
public interface ThrowingSupplier<T, E extends Throwable> {
5+
T get() throws E;
6+
}

src/main/java/net/marcellperger/mathexpr/util/Util.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,28 @@ public void forEachRemaining(Consumer<? super T> action) {
257257
return VoidVal.val();
258258
};
259259
}
260+
261+
// These 2 trick Java into throwing checked exceptions in an unchecked way
262+
@Contract("_ -> fail")
263+
public static <T> T throwAsUnchecked(Throwable exc) {
264+
throwAs(exc); // <RuntimeException> is inferred from no `throws` clause
265+
// Java doesn't know that this always throws so this lets us
266+
// do `return/throw throwAsUnchecked()` to make Java's flow control analyser happy
267+
throw new AssertionError("Unreachable");
268+
}
269+
@SuppressWarnings("unchecked")
270+
@Contract("_ -> fail")
271+
public static <E extends Throwable> void throwAs(Throwable exc) throws E {
272+
// We do a little type erasure hack to trick Java:
273+
// - E will be type-erased to Throwable so this will become
274+
// throw (Throwable)exc;
275+
// but exc is already Throwable due to the param type so
276+
// this is like a runtime no-op.
277+
// - But javac will see that we're throwing an E which is allowed!
278+
// - The reason we need a separate method is so that there is a
279+
// generic for javac to type-erase (otherwise JVM would check that
280+
// it's actually that concrete type when it is thrown and this way
281+
// it only checks for the base condition)
282+
throw (E)exc;
283+
}
260284
}

src/main/java/net/marcellperger/mathexpr/util/rs/Result.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.marcellperger.mathexpr.util.rs;
22

3+
import net.marcellperger.mathexpr.util.ThrowingSupplier;
34
import net.marcellperger.mathexpr.util.Util;
45
import org.jetbrains.annotations.NotNull;
56
import org.jetbrains.annotations.Nullable;
@@ -51,6 +52,19 @@ static <T, E> Result<T, E> newErr(E err) {
5152
return new Err<>(err);
5253
}
5354

55+
static <T, E extends Throwable> Result<T, E> fromTry(ThrowingSupplier<? extends T, E> inner, Class<E> catchThis) {
56+
try {
57+
return newOk(inner.get());
58+
} catch (Throwable exc) {
59+
try {
60+
return newErr(catchThis.cast(exc));
61+
} catch (ClassCastException c) {
62+
// We've handled E, so only unchecked exceptions should reach here
63+
// so it's safe to throw them (but Java doesn't know that so we trick it)
64+
return Util.throwAsUnchecked(exc);
65+
}
66+
}
67+
}
5468

5569
default @Nullable Ok<T, E> ok() {
5670
return switch (this) {

src/test/java/net/marcellperger/mathexpr/util/rs/ResultTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,4 +442,41 @@ void errOption() {
442442
assertEquals(Option.newNone(), getOk().errOption());
443443
assertEquals(Option.newSome("TESTING_ERROR"), getErr().errOption());
444444
}
445+
446+
@SuppressWarnings("CommentedOutCode")
447+
@Test
448+
void fromTry() {
449+
assertEquals(Result.newOk(314), Result.fromTry(() -> 314, CustomException.class));
450+
CustomException ce = new CustomException("Unchecked (expected) exception");
451+
CheckedCustomException cce = new CheckedCustomException("Checked (expected) exception");
452+
assertEquals(Result.newErr(cce), Result.fromTry(() -> {throw cce;}, CheckedCustomException.class));
453+
assertEquals(Result.newErr(ce), Result.fromTry(() -> {throw ce;}, CustomException.class));
454+
UnexpectedCustomException uce = new UnexpectedCustomException("Unexpected unchecked exc");
455+
assertThrows(UnexpectedCustomException.class, () -> Result.fromTry(() -> {throw uce;}, CustomException.class));
456+
// This doesn't compile so GOOD! (How do I write a test that something DOESN'T compile???)
457+
// UnexpectedCheckedCustomException ucc = new UnexpectedCheckedCustomException("Unexpected checked exc");
458+
// assertThrows(UnexpectedCheckedCustomException.class, () -> Result.fromTry(() -> {throw ucc;}, CheckedCustomException.class));
459+
}
460+
461+
static class UnexpectedCustomException extends RuntimeException {
462+
public UnexpectedCustomException(String message) {
463+
super(message);
464+
}
465+
}
466+
static class CustomException extends RuntimeException {
467+
public CustomException(String message) {
468+
super(message);
469+
}
470+
}
471+
static class CheckedCustomException extends Exception {
472+
public CheckedCustomException(String message) {
473+
super(message);
474+
}
475+
}
476+
@SuppressWarnings("unused") // used in the does-not-compile test
477+
static class UnexpectedCheckedCustomException extends Exception {
478+
public UnexpectedCheckedCustomException(String message) {
479+
super(message);
480+
}
481+
}
445482
}

0 commit comments

Comments
 (0)