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, E> error;
+
+ private R1(A value1, Result, E> 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, E> error;
+
+ private R2(A value1, B value2, Result, E> 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, E> error;
+
+ private R3(A value1, B value2, C value3, Result, E> 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, E> error;
+
+ private R4(A value1, B value2, C value3, D value4, Result, E> 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, F> error;
+
+ private R5(A value1, B value2, C value3, D value4, E value5, Result, F> 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, G> error;
+
+ private R6(
+ A value1,
+ B value2,
+ C value3,
+ D value4,
+ E value5,
+ F value6,
+ Result, G> 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, H> error;
+
+ private R7(
+ A value1,
+ B value2,
+ C value3,
+ D value4,
+ E value5,
+ F value6,
+ G value7,
+ Result, H> 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, I> error;
+
+ private R8(
+ A value1,
+ B value2,
+ C value3,
+ D value4,
+ E value5,
+ F value6,
+ G value7,
+ H value8,
+ Result, I> 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, J> error;
+
+ private R9(
+ A value1,
+ B value2,
+ C value3,
+ D value4,
+ E value5,
+ F value6,
+ G value7,
+ H value8,
+ I value9,
+ Result, J> 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);
+ }
}