Created
November 11, 2015 05:03
-
-
Save david-bakin/35d55daeeaee1eb71cea to your computer and use it in GitHub Desktop.
Java Result class - union (sum) type of a success type and a failure type, with a "pattern matching" functional interface
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.bakins_bits.types; | |
import java.util.function.Consumer; | |
import java.util.function.Function; | |
import java.util.function.Supplier; | |
import org.apache.commons.lang3.builder.EqualsBuilder; | |
import org.apache.commons.lang3.builder.HashCodeBuilder; | |
import com.google.common.base.Preconditions; | |
/** | |
* Hold a result that can either be a successful value, or an failure (error) value. Use instead of exceptions for a | |
* more straightforward style (IMO) but <i>don't forget to use the return value of any method returning a Result!</i>. | |
* If your success or failure needs no additional information beyond the simple fact of success or failure, use type | |
* Void or a wildcard ? (and the parameterless factories success() and failure()). | |
* <p> | |
* Inspired by | |
* <a href="http://enterprisecraftsmanship.com/2015/03/20/functional-c-handling-failures-input-errors/">Functional C#: | |
* Handling failures, input errors</a> and the code | |
* <a href="https://gist.github.com/vkhorikov/7852c7606f27c52bc288">here</a>. | |
* <p> | |
* (I wish C-family languages, including the O-S variants like C# and Java, had an ability to force a compiler error on | |
* ignoring the result from a function/method (unless it was deliberately thrown away with a cast to void). Perhaps as | |
* an optional keyword on the function/method declaration.) | |
*/ | |
public abstract class Result<S, F> | |
{ | |
private Result() { | |
} | |
/** | |
* Return a success result. | |
*/ | |
public static <S, F> Result<S, F> success(S s) { | |
return new SResult<S, F>(s); | |
} | |
/** | |
* Return a success result with no value. | |
*/ | |
public static <S, F> Result<S, F> success() { | |
return new SResult<S, F>(null); | |
} | |
/** | |
* Return an error result. | |
*/ | |
public static <S, F> Result<S, F> fail(F f) { | |
return new FResult<S, F>(f); | |
} | |
/** | |
* Return an error result with no value. | |
*/ | |
public static <S, F> Result<S, F> fail() { | |
return new FResult<S, F>(null); | |
} | |
protected abstract Object getValue(); | |
/** | |
* Return true iff this is a success result. | |
*/ | |
public boolean isSuccess() { | |
return !isFailure(); | |
} | |
/** | |
* Return true iff this is a failure result. | |
*/ | |
public boolean isFailure() { | |
return !isSuccess(); | |
} | |
/** | |
* Given a bunch of results, return the first fail, or if none, return a generic success (with void value). | |
*/ | |
@SafeVarargs | |
public static <S, F> Result<S, F> combine(Result<S, F>... results) { | |
for (Result<S, F> r : results) { | |
Preconditions.checkNotNull(r, "no element of params results can be null"); | |
if (r.isFailure()) | |
return r; | |
} | |
return success(); | |
} | |
// TODO: A version of combine that takes a combiner Function<F[], FF> to combine the failure values. | |
/** | |
* Continuation for successful values, given a 0-param lambda that returns a new Result. Passes failure through. | |
*/ | |
public <SS> Result<SS, F> onSuccess(Supplier<Result<SS, F>> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
if (isSuccess()) | |
return func.get(); | |
else { | |
@SuppressWarnings("unchecked") | |
F f = (F) getValue(); | |
return fail(f); | |
} | |
} | |
/** | |
* Continuation for successful values, given a 1-param lambda that returns void. Passes failure through. | |
*/ | |
public Result<S, F> onSuccess(Consumer<S> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
if (isSuccess()) { | |
@SuppressWarnings("unchecked") | |
S s = (S) getValue(); | |
func.accept(s); | |
} | |
return this; | |
} | |
/** | |
* Continuation for successful values, given a function that transforms a success value. Passes failure through. | |
*/ | |
public <SS> Result<SS, F> onSuccess(Function<S, SS> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
if (isSuccess()) { | |
@SuppressWarnings("unchecked") | |
S s = (S) getValue(); | |
SS ss = func.apply(s); | |
return success(ss); | |
} else { | |
@SuppressWarnings("unchecked") | |
F f = (F) getValue(); | |
return fail(f); | |
} | |
} | |
/** | |
* Continuation for failure values, given a 0-param lambda that returns a new Result. Passes success through. | |
*/ | |
public <FF> Result<S, FF> onFailure(Supplier<Result<S, FF>> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
if (isFailure()) | |
return func.get(); | |
else { | |
@SuppressWarnings("unchecked") | |
S s = (S) getValue(); | |
return success(s); | |
} | |
} | |
/** | |
* Continuation for failure values, given a 1-param lambda that returns void. Passes success through. | |
*/ | |
public Result<S, F> onFailure(Consumer<F> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
if (isFailure()) { | |
@SuppressWarnings("unchecked") | |
F f = (F) getValue(); | |
func.accept(f); | |
return this; | |
} | |
return this; | |
} | |
/** | |
* Continuation for failure values, given a function that transforms an error value. Passes success through. | |
*/ | |
public <FF> Result<S, FF> onFailure(Function<F, FF> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
if (isFailure()) { | |
@SuppressWarnings("unchecked") | |
F f = (F) getValue(); | |
FF ff = func.apply(f); | |
return fail(ff); | |
} else { | |
@SuppressWarnings("unchecked") | |
S s = (S) getValue(); | |
return success(s); | |
} | |
} | |
/** | |
* Continuation for Result values (success or failure), given a 1-param lambda that returns void. Passes the | |
* original Result through. | |
*/ | |
public Result<S, F> onBoth(Consumer<Result<S, F>> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
func.accept(this); | |
return this; | |
} | |
/** | |
* Continuation for Result values (success or failure), given a function that transforms this Result into another | |
* Result. | |
*/ | |
public <SS, FF> Result<SS, FF> onBoth(Function<Result<S, F>, Result<SS, FF>> func) { | |
Preconditions.checkNotNull(func, "func must not be null"); | |
return func.apply(this); | |
} | |
@Override | |
public boolean equals(Object obj) { | |
if (null == obj) | |
return false; | |
if (this == obj) | |
return true; | |
if (this.getClass() != obj.getClass()) | |
return false; | |
@SuppressWarnings("rawtypes") | |
Result rhs = (Result) obj; | |
return new EqualsBuilder().append(this.getValue(), rhs.getValue()).isEquals(); | |
} | |
@Override | |
public int hashCode() { | |
return new HashCodeBuilder(1069, 9601 /* emrips */).append(getValue()).toHashCode(); | |
} | |
private static class SResult<S, F> extends Result<S, F> | |
{ | |
private final S s; | |
private SResult(S s) { | |
this.s = s; | |
} | |
@Override | |
protected S getValue() { | |
return s; | |
} | |
@Override | |
public boolean isSuccess() { | |
return true; | |
} | |
@Override | |
public String toString() { | |
return "S:" + String.valueOf(s); | |
} | |
} | |
private static class FResult<S, F> extends Result<S, F> | |
{ | |
private final F f; | |
private FResult(F f) { | |
this.f = f; | |
} | |
@Override | |
protected F getValue() { | |
return f; | |
} | |
@Override | |
public boolean isFailure() { | |
return true; | |
} | |
@Override | |
public String toString() { | |
return "F:" + String.valueOf(f); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.bakins_bits.types; | |
import static org.assertj.core.api.Assertions.*; | |
import org.testng.annotations.Test; | |
/** | |
* Test class Result<S,F> | |
*/ | |
public class TestResult | |
{ | |
// This little dance with field b and methods setB and resetB is to get around the "final" capture of Java lambdas. | |
public boolean b; | |
public void resetB() { | |
b = false; | |
} | |
public void setB() { | |
b = true; | |
} | |
@Test(groups = { "unit" }) | |
public void success_is_success() { | |
// ARRANGE & ACT | |
Result<Integer, String> sut = Result.success(5); | |
// ASSERT | |
resetB(); | |
assertThat(sut.isSuccess()).isTrue(); | |
assertThat(sut.isFailure()).isFalse(); | |
sut.onSuccess(i -> { | |
setB(); | |
assertThat(i).isEqualTo(5); | |
}).onFailure(﹏ -> { | |
fail("called OnFailure"); | |
}); | |
assertThat(b).isTrue(); | |
} | |
@Test(groups = { "unit" }) | |
public void failure_is_failure() { | |
// ARRANGE & ACT | |
Result<Integer, String> sut = Result.fail("fubar"); | |
// ASSERT | |
resetB(); | |
assertThat(sut.isSuccess()).isFalse(); | |
assertThat(sut.isFailure()).isTrue(); | |
sut.onFailure(s -> { | |
setB(); | |
assertThat(s).isEqualTo("fubar"); | |
}).onSuccess(﹏ -> { | |
fail("called OnSuccess"); | |
}); | |
assertThat(b).isTrue(); | |
} | |
@Test(groups = { "unit" }) | |
public void success_type_can_be_Void() { | |
// ARRANGE & ACT | |
Result<Void, String> sut = Result.success(); | |
// ASSERT | |
assertThat(sut.isSuccess()).isTrue(); | |
assertThat(sut.isFailure()).isFalse(); | |
} | |
@Test(groups = { "unit" }) | |
public void success_type_can_be_unbounded_wildcard() { | |
// ARRANGE & ACT | |
Result<?, String> sut = Result.success(); | |
// ASSERT | |
assertThat(sut.isSuccess()).isTrue(); | |
assertThat(sut.isFailure()).isFalse(); | |
} | |
@Test(groups = { "unit" }) | |
public void combine_with_no_fails_returns_success() { | |
// ARRANGE | |
Result<Integer, String> sut1 = Result.success(5); | |
Result<Integer, String> sut2 = Result.success(15); | |
Result<Integer, String> sut3 = Result.success(20); | |
Result<Integer, String> sut4 = Result.success(25); | |
Result<Integer, String> sut5 = Result.success(30); | |
// ACT | |
Result<Integer, String> actual = Result.combine(sut1, sut2, sut3, sut4, sut5); | |
// ASSERT | |
assertThat(actual).isEqualTo(Result.success()); | |
} | |
@Test(groups = { "unit" }) | |
public void combine_returns_first_fail() { | |
// ARRANGE | |
Result<Integer, String> sut1 = Result.success(5); | |
Result<Integer, String> sut2 = Result.success(15); | |
Result<Integer, String> sut3 = Result.fail("foo"); | |
Result<Integer, String> sut4 = Result.success(25); | |
Result<Integer, String> sut5 = Result.fail("bar"); | |
// ACT | |
Result<Integer, String> actual = Result.combine(sut1, sut2, sut3, sut4, sut5); | |
// ASSERT | |
assertThat(actual).isEqualTo(sut3); | |
} | |
@Test(groups = { "unit" }) | |
public void onSuccess_taking_supplier_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT | |
resetB(); | |
Result<String, String> actualS = sutS.onSuccess(() -> { | |
setB(); | |
return Result.<String, String> success("15"); | |
}).onFailure(﹏ -> { | |
fail("onFailure called, sutS"); | |
}); | |
Result<String, String> actualF = sutF.onSuccess(() -> { | |
fail("onSuccess called, sutF"); | |
return Result.success("x"); | |
}); | |
// ASSERT | |
assertThat(b).isTrue(); | |
assertThat(actualS.isSuccess()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo("15"); | |
assertThat(actualF.isFailure()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo("foo"); | |
} | |
@Test(groups = { "unit" }) | |
public void onFailure_taking_supplier_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT | |
resetB(); | |
Result<Integer, Integer> actualF = sutF.onFailure(() -> { | |
setB(); | |
return Result.<Integer, Integer> fail(15); | |
}).onSuccess(﹏ -> { | |
fail("onSuccess called, sutF"); | |
}); | |
Result<Integer, Integer> actualS = sutS.onFailure(() -> { | |
fail("onFailure called, sutS"); | |
return Result.fail(10); | |
}); | |
// ASSERT | |
assertThat(b).isTrue(); | |
assertThat(actualF.isFailure()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo(15); | |
assertThat(actualS.isSuccess()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo(15); | |
} | |
@Test(groups = { "unit" }) | |
public void onSuccess_taking_consumer_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT | |
resetB(); | |
Result<Integer, String> actualS = sutS.onSuccess(s -> { | |
setB(); | |
assertThat(s).isEqualTo(15); | |
}).onFailure(﹏ -> { | |
fail("onFailure called, sutS"); | |
}); | |
Result<Integer, String> actualF = sutF.onSuccess(() -> { | |
fail("onSuccess called, sutF"); | |
return Result.success(20); | |
}); | |
// ASSERT | |
assertThat(b).isTrue(); | |
assertThat(actualS.isSuccess()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo(15); | |
assertThat(actualF.isFailure()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo("foo"); | |
} | |
@Test(groups = { "unit" }) | |
public void onFailure_taking_consumer_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT | |
resetB(); | |
Result<Integer, String> actualF = sutF.onFailure(f -> { | |
setB(); | |
assertThat(f).isEqualTo("foo"); | |
}).onSuccess(﹏ -> { | |
fail("onSuccess called, sutF"); | |
}); | |
Result<Integer, String> actualS = sutS.onFailure(() -> { | |
fail("onFailure called, sutS"); | |
return Result.success(25); | |
}); | |
// ASSERT | |
assertThat(b).isTrue(); | |
assertThat(actualF.isFailure()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo("foo"); | |
assertThat(actualS.isSuccess()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo(15); | |
} | |
@Test(groups = { "unit" }) | |
public void onSuccess_taking_transformer_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT | |
resetB(); | |
Result<String, String> actualS = sutS.onSuccess(s -> { | |
setB(); | |
assertThat(s).isEqualTo(15); | |
return "bar"; | |
}).onFailure(﹏ -> { | |
fail("onFailure called, sutS"); | |
}); | |
Result<String, String> actualF = sutF.onSuccess(() -> { | |
fail("onSuccess called, sutF"); | |
return Result.success("xray"); | |
}); | |
// ASSERT | |
assertThat(b).isTrue(); | |
assertThat(actualS.isSuccess()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo("bar"); | |
assertThat(actualF.isFailure()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo("foo"); | |
} | |
@Test(groups = { "unit" }) | |
public void onFailure_taking_transformer_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT | |
resetB(); | |
Result<Integer, Integer> actualF = sutF.onFailure(f -> { | |
setB(); | |
assertThat(f).isEqualTo("foo"); | |
return 45; | |
}).onSuccess(﹏ -> { | |
fail("onSuccess called, sutF"); | |
}); | |
Result<Integer, Integer> actualS = sutS.onFailure(() -> { | |
fail("onFailure called, sutS"); | |
return Result.success(25); | |
}); | |
// ASSERT | |
assertThat(b).isTrue(); | |
assertThat(actualF.isFailure()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo(45); | |
assertThat(actualS.isSuccess()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo(15); | |
} | |
@Test(groups = { "unit" }) | |
public void onBoth_taking_consumer_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT & ASSERT | |
resetB(); | |
Result<Integer, String> actualS = sutS.onBoth(r -> { | |
setB(); | |
assertThat(r).isEqualTo(sutS); | |
}); | |
assertThat(b).isTrue(); | |
assertThat(actualS).isEqualTo(sutS); | |
resetB(); | |
Result<Integer, String> actualF = sutF.onBoth(r -> { | |
setB(); | |
assertThat(r).isEqualTo(sutF); | |
}); | |
assertThat(b).isTrue(); | |
assertThat(actualF).isEqualTo(sutF); | |
} | |
@Test(groups = { "unit" }) | |
public void onBoth_taking_transformer_works() { | |
// ARRANGE | |
Result<Integer, String> sutS = Result.success(15); | |
Result<Integer, String> sutF = Result.fail("foo"); | |
// ACT & ASSERT | |
resetB(); | |
Result<String, Integer> actualS = sutS.onBoth(r -> { | |
setB(); | |
assertThat(r).isEqualTo(sutS); | |
return Result.fail(40); | |
}); | |
assertThat(b).isTrue(); | |
assertThat(actualS.isFailure()).isTrue(); | |
assertThat(actualS.getValue()).isEqualTo(40); | |
resetB(); | |
Result<String, Integer> actualF = sutF.onBoth(r -> { | |
setB(); | |
assertThat(r).isEqualTo(sutF); | |
return Result.success("xray"); | |
}); | |
assertThat(b).isTrue(); | |
assertThat(actualF.isSuccess()).isTrue(); | |
assertThat(actualF.getValue()).isEqualTo("xray"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment