diff --git a/algebra/src/main/java/com/hubspot/algebra/NonFunction.java b/algebra/src/main/java/com/hubspot/algebra/NonFunction.java new file mode 100644 index 0000000..e5326de --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/NonFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface NonFunction { + R apply(A a, B b, C c, D d, E e, F f, G g, H h, I i); +} diff --git a/algebra/src/main/java/com/hubspot/algebra/OctFunction.java b/algebra/src/main/java/com/hubspot/algebra/OctFunction.java new file mode 100644 index 0000000..a13cb9e --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/OctFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface OctFunction { + R apply(A a, B b, C c, D d, E e, F f, G g, H h); +} diff --git a/algebra/src/main/java/com/hubspot/algebra/QuadFunction.java b/algebra/src/main/java/com/hubspot/algebra/QuadFunction.java new file mode 100644 index 0000000..4cad5fe --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/QuadFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface QuadFunction { + R apply(A a, B b, C c, D d); +} diff --git a/algebra/src/main/java/com/hubspot/algebra/QuintFunction.java b/algebra/src/main/java/com/hubspot/algebra/QuintFunction.java new file mode 100644 index 0000000..d9a52fb --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/QuintFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface QuintFunction { + R apply(A a, B b, C c, D d, E e); +} diff --git a/algebra/src/main/java/com/hubspot/algebra/Result.java b/algebra/src/main/java/com/hubspot/algebra/Result.java index d1cdd65..c2a5c05 100644 --- a/algebra/src/main/java/com/hubspot/algebra/Result.java +++ b/algebra/src/main/java/com/hubspot/algebra/Result.java @@ -26,6 +26,20 @@ public static Result nullErr() { return Results.err(NullValue.get()); } + /** + * Start combining multiple Results with type-safety. + * Each Result is accumulated while maintaining its type information, + * allowing direct access to all values in the combiner function. + * + * @param Type of the first Result's value + * @param Type of the error + * @param r1 First Result to combine + * @return A builder for further combination + */ + public static ResultCombinator.R1 combine(Result r1) { + return ResultCombinator.combine(r1); + } + Result() {} public boolean isOk() { diff --git a/algebra/src/main/java/com/hubspot/algebra/ResultCombinator.java b/algebra/src/main/java/com/hubspot/algebra/ResultCombinator.java new file mode 100644 index 0000000..e704bff --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/ResultCombinator.java @@ -0,0 +1,557 @@ +package com.hubspot.algebra; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * A type-accumulating builder for combining Results. + * This approach builds up the type information through each step, + * allowing type-safe access to all accumulated values. + */ +class ResultCombinator { + + /** + * Start combining with one Result. + */ + public static R1 combine(Result r1) { + if (r1.isErr()) { + return new R1<>(null, r1.coerceErr()); + } + return new R1<>(r1.unwrapOrElseThrow(), null); + } + + /** + * Holder for one Result value. + */ + public static class R1 { + + private final A value1; + private final Result error; + + private R1(A value1, Result error) { + this.value1 = value1; + this.error = error; + } + + /** + * Add a second Result to be combined. + */ + public R2 and(Result r2) { + if (error != null) { + return new R2<>(value1, null, error); + } + if (r2.isErr()) { + return new R2<>(value1, null, r2.coerceErr()); + } + return new R2<>(value1, r2.unwrapOrElseThrow(), null); + } + + /** + * Complete the combination with a mapping function for one value. + */ + public Result map(Function mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok(mapper.apply(value1)); + } + } + + /** + * Holder for two Result values. + */ + public static class R2 { + + private final A value1; + private final B value2; + private final Result error; + + private R2(A value1, B value2, Result error) { + this.value1 = value1; + this.value2 = value2; + this.error = error; + } + + /** + * Add a third Result to be combined. + */ + public R3 and(Result r3) { + if (error != null) { + return new R3<>(value1, value2, null, error); + } + if (r3.isErr()) { + return new R3<>(value1, value2, null, r3.coerceErr()); + } + return new R3<>(value1, value2, r3.unwrapOrElseThrow(), null); + } + + /** + * Complete the combination with a mapping function for two values. + */ + public Result map(BiFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok(mapper.apply(value1, value2)); + } + } + + /** + * Holder for three Result values. + */ + public static class R3 { + + private final A value1; + private final B value2; + private final C value3; + private final Result error; + + private R3(A value1, B value2, C value3, Result error) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.error = error; + } + + /** + * Add a fourth Result to be combined. + */ + public R4 and(Result r4) { + if (error != null) { + return new R4<>(value1, value2, value3, null, error); + } + if (r4.isErr()) { + return new R4<>(value1, value2, value3, null, r4.coerceErr()); + } + return new R4<>(value1, value2, value3, r4.unwrapOrElseThrow(), null); + } + + /** + * Complete the combination with a mapping function for three values. + */ + public Result map(com.hubspot.algebra.TriFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok(mapper.apply(value1, value2, value3)); + } + } + + /** + * Holder for four Result values. + */ + public static class R4 { + + private final A value1; + private final B value2; + private final C value3; + private final D value4; + private final Result error; + + private R4(A value1, B value2, C value3, D value4, Result error) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.error = error; + } + + /** + * Add a fifth Result to be combined. + */ + public R5 and(Result r5) { + if (error != null) { + return new R5<>(value1, value2, value3, value4, null, error); + } + if (r5.isErr()) { + return new R5<>(value1, value2, value3, value4, null, r5.coerceErr()); + } + return new R5<>(value1, value2, value3, value4, r5.unwrapOrElseThrow(), null); + } + + /** + * Complete the combination with a mapping function for four values. + */ + public Result map(QuadFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok(mapper.apply(value1, value2, value3, value4)); + } + } + + /** + * Holder for five Result values. + */ + public static class R5 { + + private final A value1; + private final B value2; + private final C value3; + private final D value4; + private final E value5; + private final Result error; + + private R5(A value1, B value2, C value3, D value4, E value5, Result error) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.error = error; + } + + /** + * Add a sixth Result to be combined. + */ + public R6 and(Result r6) { + if (error != null) { + return new R6<>(value1, value2, value3, value4, value5, null, error); + } + if (r6.isErr()) { + return new R6<>(value1, value2, value3, value4, value5, null, r6.coerceErr()); + } + return new R6<>( + value1, + value2, + value3, + value4, + value5, + r6.unwrapOrElseThrow(), + null + ); + } + + /** + * Complete the combination with a mapping function for five values. + */ + public Result map(QuintFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok(mapper.apply(value1, value2, value3, value4, value5)); + } + } + + /** + * Holder for six Result values. + */ + public static class R6 { + + private final A value1; + private final B value2; + private final C value3; + private final D value4; + private final E value5; + private final F value6; + private final Result error; + + private R6( + A value1, + B value2, + C value3, + D value4, + E value5, + F value6, + Result error + ) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + this.error = error; + } + + /** + * Add a seventh Result to be combined. + */ + public R7 and(Result r7) { + if (error != null) { + return new R7<>(value1, value2, value3, value4, value5, value6, null, error); + } + if (r7.isErr()) { + return new R7<>( + value1, + value2, + value3, + value4, + value5, + value6, + null, + r7.coerceErr() + ); + } + return new R7<>( + value1, + value2, + value3, + value4, + value5, + value6, + r7.unwrapOrElseThrow(), + null + ); + } + + /** + * Complete the combination with a mapping function for six values. + */ + public Result map(SextFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok(mapper.apply(value1, value2, value3, value4, value5, value6)); + } + } + + /** + * Holder for seven Result values. + */ + public static class R7 { + + private final A value1; + private final B value2; + private final C value3; + private final D value4; + private final E value5; + private final F value6; + private final G value7; + private final Result error; + + private R7( + A value1, + B value2, + C value3, + D value4, + E value5, + F value6, + G value7, + Result error + ) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + this.value7 = value7; + this.error = error; + } + + /** + * Add an eighth Result to be combined. + */ + public R8 and(Result r8) { + if (error != null) { + return new R8<>( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + null, + error + ); + } + if (r8.isErr()) { + return new R8<>( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + null, + r8.coerceErr() + ); + } + return new R8<>( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + r8.unwrapOrElseThrow(), + null + ); + } + + /** + * Complete the combination with a mapping function for seven values. + */ + public Result map(SeptFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok( + mapper.apply(value1, value2, value3, value4, value5, value6, value7) + ); + } + } + + /** + * Holder for eight Result values. + */ + public static class R8 { + + private final A value1; + private final B value2; + private final C value3; + private final D value4; + private final E value5; + private final F value6; + private final G value7; + private final H value8; + private final Result error; + + private R8( + A value1, + B value2, + C value3, + D value4, + E value5, + F value6, + G value7, + H value8, + Result error + ) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + this.value7 = value7; + this.value8 = value8; + this.error = error; + } + + /** + * Add a ninth Result to be combined. + */ + public R9 and(Result r9) { + if (error != null) { + return new R9<>( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + value8, + null, + error + ); + } + if (r9.isErr()) { + return new R9<>( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + value8, + null, + r9.coerceErr() + ); + } + return new R9<>( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + value8, + r9.unwrapOrElseThrow(), + null + ); + } + + /** + * Complete the combination with a mapping function for eight values. + */ + public Result map(OctFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok( + mapper.apply(value1, value2, value3, value4, value5, value6, value7, value8) + ); + } + } + + /** + * Holder for nine Result values. + */ + public static class R9 { + + private final A value1; + private final B value2; + private final C value3; + private final D value4; + private final E value5; + private final F value6; + private final G value7; + private final H value8; + private final I value9; + private final Result error; + + private R9( + A value1, + B value2, + C value3, + D value4, + E value5, + F value6, + G value7, + H value8, + I value9, + Result error + ) { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + this.value7 = value7; + this.value8 = value8; + this.value9 = value9; + this.error = error; + } + + /** + * Complete the combination with a mapping function for nine values. + */ + public Result map(NonFunction mapper) { + if (error != null) { + return error.coerceErr(); + } + return Result.ok( + mapper.apply( + value1, + value2, + value3, + value4, + value5, + value6, + value7, + value8, + value9 + ) + ); + } + } +} diff --git a/algebra/src/main/java/com/hubspot/algebra/SeptFunction.java b/algebra/src/main/java/com/hubspot/algebra/SeptFunction.java new file mode 100644 index 0000000..d3b234d --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/SeptFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface SeptFunction { + R apply(A a, B b, C c, D d, E e, F f, G g); +} diff --git a/algebra/src/main/java/com/hubspot/algebra/SextFunction.java b/algebra/src/main/java/com/hubspot/algebra/SextFunction.java new file mode 100644 index 0000000..a60569d --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/SextFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface SextFunction { + R apply(A a, B b, C c, D d, E e, F f); +} diff --git a/algebra/src/main/java/com/hubspot/algebra/TriFunction.java b/algebra/src/main/java/com/hubspot/algebra/TriFunction.java new file mode 100644 index 0000000..23cda7f --- /dev/null +++ b/algebra/src/main/java/com/hubspot/algebra/TriFunction.java @@ -0,0 +1,6 @@ +package com.hubspot.algebra; + +@FunctionalInterface +public interface TriFunction { + R apply(A a, B b, C c); +} diff --git a/algebra/src/test/java/com/hubspot/algebra/ResultTest.java b/algebra/src/test/java/com/hubspot/algebra/ResultTest.java index 20933df..3f98968 100644 --- a/algebra/src/test/java/com/hubspot/algebra/ResultTest.java +++ b/algebra/src/test/java/com/hubspot/algebra/ResultTest.java @@ -215,4 +215,173 @@ public void itThrowsWhenCoerceErrCalledOnOk() { Result okResult = Result.ok(SAMPLE_STRING); okResult.coerceErr(); } + + @Test + public void itCombinesTwoOkResults() { + Result result1 = Result.ok("Hello"); + Result result2 = Result.ok(42); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .map((str, num) -> str + " " + num); + + assertThat(combinedResult.isOk()).isTrue(); + assertThat(combinedResult.unwrapOrElseThrow()).isEqualTo("Hello 42"); + } + + @Test + public void itCombinesOkAndErrResults() { + Result result1 = Result.ok("Hello"); + Result result2 = Result.err(SampleError.TEST_ERROR); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .map((str, num) -> str + " " + num); + + assertThat(combinedResult.isErr()).isTrue(); + assertThat(combinedResult.unwrapErrOrElseThrow()).isEqualTo(SampleError.TEST_ERROR); + } + + @Test + public void itCombinesTwoErrResults() { + Result result1 = Result.err(SampleError.TEST_ERROR); + Result result2 = Result.err(SampleError.TEST_ERROR_TWO); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .map((str, num) -> str + " " + num); + + assertThat(combinedResult.isErr()).isTrue(); + assertThat(combinedResult.unwrapErrOrElseThrow()).isEqualTo(SampleError.TEST_ERROR); + } + + @Test + public void itCombinesFiveOkResults() { + Result result1 = Result.ok("Hello"); + Result result2 = Result.ok(42); + Result result3 = Result.ok(3.14); + Result result4 = Result.ok(true); + Result result5 = Result.ok('A'); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .and(result3) + .and(result4) + .and(result5) + .map((str, num, dbl, bool, chr) -> + str + " " + num + " " + dbl + " " + bool + " " + chr + ); + + assertThat(combinedResult.isOk()).isTrue(); + assertThat(combinedResult.unwrapOrElseThrow()).isEqualTo("Hello 42 3.14 true A"); + } + + @Test + public void itCombinesFiveResultsWithAnErr() { + Result result1 = Result.ok("Hello"); + Result result2 = Result.err(SampleError.TEST_ERROR); + Result result3 = Result.ok(3.14); + Result result4 = Result.ok(true); + Result result5 = Result.ok('A'); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .and(result3) + .and(result4) + .and(result5) + .map((str, num, dbl, bool, chr) -> + str + " " + num + " " + dbl + " " + bool + " " + chr + ); + + assertThat(combinedResult.isErr()).isTrue(); + assertThat(combinedResult.unwrapErrOrElseThrow()).isEqualTo(SampleError.TEST_ERROR); + } + + @Test + public void itCombinesNineOkResults() { + Result result1 = Result.ok("Hello"); + Result result2 = Result.ok(42); + Result result3 = Result.ok(3.14); + Result result4 = Result.ok(true); + Result result5 = Result.ok('A'); + Result result6 = Result.ok(123L); + Result result7 = Result.ok(2.718f); + Result result8 = Result.ok((short) 7); + Result result9 = Result.ok((byte) 1); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .and(result3) + .and(result4) + .and(result5) + .and(result6) + .and(result7) + .and(result8) + .and(result9) + .map((str, num, dbl, bool, chr, lng, flt, shrt, byt) -> + String.format( + "%s %d %.2f %b %c %d %.3f %d %d", + str, + num, + dbl, + bool, + chr, + lng, + flt, + shrt, + byt + ) + ); + + assertThat(combinedResult.isOk()).isTrue(); + assertThat(combinedResult.unwrapOrElseThrow()) + .isEqualTo("Hello 42 3.14 true A 123 2.718 7 1"); + } + + @Test + public void itCombinesNineResultsWithAnErr() { + Result result1 = Result.ok("Hello"); + Result result2 = Result.ok(42); + Result result3 = Result.ok(3.14); + Result result4 = Result.err(SampleError.TEST_ERROR); + Result result5 = Result.ok('A'); + Result result6 = Result.ok(123L); + Result result7 = Result.ok(2.718f); + Result result8 = Result.ok((short) 7); + Result result9 = Result.ok((byte) 1); + + Result combinedResult = Result + .combine(result1) + .and(result2) + .and(result3) + .and(result4) + .and(result5) + .and(result6) + .and(result7) + .and(result8) + .and(result9) + .map((str, num, dbl, bool, chr, lng, flt, shrt, byt) -> + String.format( + "%s %d %.2f %b %c %d %.3f %d %d", + str, + num, + dbl, + bool, + chr, + lng, + flt, + shrt, + byt + ) + ); + + assertThat(combinedResult.isErr()).isTrue(); + assertThat(combinedResult.unwrapErrOrElseThrow()).isEqualTo(SampleError.TEST_ERROR); + } }