Skip to content

Commit

Permalink
refactor: Add Result.fromTry
Browse files Browse the repository at this point in the history
(May or may not involve tricking the compiler)
  • Loading branch information
MarcellPerger1 committed May 26, 2024
1 parent 75e0bb3 commit ae7b7cd
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.marcellperger.mathexpr.util;

@FunctionalInterface
public interface ThrowingSupplier<T, E extends Throwable> {
T get() throws E;
}
24 changes: 24 additions & 0 deletions src/main/java/net/marcellperger/mathexpr/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,28 @@ public void forEachRemaining(Consumer<? super T> action) {
return VoidVal.val();
};
}

// These 2 trick Java into throwing checked exceptions in an unchecked way
@Contract("_ -> fail")
public static <T> T throwAsUnchecked(Throwable exc) {
throwAs(exc); // <RuntimeException> is inferred from no `throws` clause
// Java doesn't know that this always throws so this lets us
// do `return/throw throwAsUnchecked()` to make Java's flow control analyser happy
throw new AssertionError("Unreachable");
}
@SuppressWarnings("unchecked")
@Contract("_ -> fail")
public static <E extends Throwable> void throwAs(Throwable exc) throws E {
// We do a little type erasure hack to trick Java:
// - E will be type-erased to Throwable so this will become
// throw (Throwable)exc;
// but exc is already Throwable due to the param type so
// this is like a runtime no-op.
// - But javac will see that we're throwing an E which is allowed!
// - The reason we need a separate method is so that there is a
// generic for javac to type-erase (otherwise JVM would check that
// it's actually that concrete type when it is thrown and this way
// it only checks for the base condition)
throw (E)exc;
}
}
14 changes: 14 additions & 0 deletions src/main/java/net/marcellperger/mathexpr/util/rs/Result.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.marcellperger.mathexpr.util.rs;

import net.marcellperger.mathexpr.util.ThrowingSupplier;
import net.marcellperger.mathexpr.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -51,6 +52,19 @@ static <T, E> Result<T, E> newErr(E err) {
return new Err<>(err);
}

static <T, E extends Throwable> Result<T, E> fromTry(ThrowingSupplier<? extends T, E> inner, Class<E> catchThis) {
try {
return newOk(inner.get());
} catch (Throwable exc) {
try {
return newErr(catchThis.cast(exc));
} catch (ClassCastException c) {
// We've handled E, so only unchecked exceptions should reach here
// so it's safe to throw them (but Java doesn't know that so we trick it)
return Util.throwAsUnchecked(exc);
}
}
}

default @Nullable Ok<T, E> ok() {
return switch (this) {
Expand Down
37 changes: 37 additions & 0 deletions src/test/java/net/marcellperger/mathexpr/util/rs/ResultTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -442,4 +442,41 @@ void errOption() {
assertEquals(Option.newNone(), getOk().errOption());
assertEquals(Option.newSome("TESTING_ERROR"), getErr().errOption());
}

@SuppressWarnings("CommentedOutCode")
@Test
void fromTry() {
assertEquals(Result.newOk(314), Result.fromTry(() -> 314, CustomException.class));
CustomException ce = new CustomException("Unchecked (expected) exception");
CheckedCustomException cce = new CheckedCustomException("Checked (expected) exception");
assertEquals(Result.newErr(cce), Result.fromTry(() -> {throw cce;}, CheckedCustomException.class));
assertEquals(Result.newErr(ce), Result.fromTry(() -> {throw ce;}, CustomException.class));
UnexpectedCustomException uce = new UnexpectedCustomException("Unexpected unchecked exc");
assertThrows(UnexpectedCustomException.class, () -> Result.fromTry(() -> {throw uce;}, CustomException.class));
// This doesn't compile so GOOD! (How do I write a test that something DOESN'T compile???)
// UnexpectedCheckedCustomException ucc = new UnexpectedCheckedCustomException("Unexpected checked exc");
// assertThrows(UnexpectedCheckedCustomException.class, () -> Result.fromTry(() -> {throw ucc;}, CheckedCustomException.class));
}

static class UnexpectedCustomException extends RuntimeException {
public UnexpectedCustomException(String message) {
super(message);
}
}
static class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
static class CheckedCustomException extends Exception {
public CheckedCustomException(String message) {
super(message);
}
}
@SuppressWarnings("unused") // used in the does-not-compile test
static class UnexpectedCheckedCustomException extends Exception {
public UnexpectedCheckedCustomException(String message) {
super(message);
}
}
}

0 comments on commit ae7b7cd

Please sign in to comment.