Add fold method to Try

This commit is contained in:
Loïc Prieto 2025-09-06 14:15:49 +02:00
parent c9700868a0
commit 965b9872cc
3 changed files with 199 additions and 1 deletions

View file

@ -6,7 +6,7 @@
<groupId>ninja.the-fire-archmage</groupId>
<artifactId>datastructure-utils</artifactId>
<version>2.0.1</version>
<version>2.1.0</version>
<properties>
<maven.compiler.source>23</maven.compiler.source>

View file

@ -407,6 +407,59 @@ public class Try<T> {
return result.isEmpty() && error.isEmpty();
}
/**
* Transforms this Try into a single value by applying one of two mapping functions.
* If this Try is successful, applies the value mapper to the contained value.
* If this Try is a failure, applies the error mapper to the contained exception.
* This operation allows for unified handling of both success and failure cases.
*
* <p>The fold operation is particularly useful when you need to:
* <ul>
* <li>Convert both success and failure cases to the same type</li>
* <li>Implement error recovery by providing default values</li>
* <li>Transform exceptions into meaningful result values</li>
* </ul>
*
* <p>Example usage:
* <pre>{@code
* // Convert both success and failure to string
* String result = Try.of(() -> Integer.parseInt("42"))
* .fold(
* value -> "Success: " + value,
* error -> "Error: " + error.getMessage()
* );
*
* // Provide default value on error
* Integer value = Try.of(() -> Integer.parseInt("invalid"))
* .fold(
* success -> success,
* error -> 0 // default value
* );
* }</pre>
*
* @param <U> the type of the result after applying either mapping function
* @param valueMapper function to apply to the value if this Try is successful
* @param errorMapper function to apply to the exception if this Try is a failure
* @return the result of applying the appropriate mapper function
* @throws IllegalArgumentException if either mapper function is null
* @throws IllegalStateException if this Try is of Void type (cannot be folded)
*/
public <U> U fold(Function<T,U> valueMapper, Function<Throwable, U> errorMapper) {
if(valueMapper == null || errorMapper == null) {
throw new IllegalArgumentException("Neither mapper can be null");
}
if(isVoidType()) {
throw new IllegalStateException("A fold operation cannot be performed when the try type is Void");
}
if(error.isPresent()) {
return errorMapper.apply(error.get());
} else if(result.isPresent()) {
return result.map(valueMapper).get();
} else {
throw new IllegalStateException("There was an error while folding the Try. No error or result present, which shouldn't be possible");
}
}
/**
* Converts a collection of Try instances into a single Try containing a List.
* If all Try instances are successful, returns a successful Try with a List of all values.

View file

@ -629,6 +629,151 @@ class TryTest {
}
}
@Nested
@DisplayName("Fold Tests")
class FoldTests {
@Test
@DisplayName("Should fold successful Try with value mapper")
void shouldFoldSuccessfulTryWithValueMapper() {
Try<Integer> tryResult = Try.success(42);
String result = tryResult.fold(
value -> "Success: " + value,
error -> "Error: " + error.getMessage()
);
assertThat(result).isEqualTo("Success: 42");
}
@Test
@DisplayName("Should fold failed Try with error mapper")
void shouldFoldFailedTryWithErrorMapper() {
RuntimeException exception = new RuntimeException("Test error");
Try<Integer> tryResult = Try.failure(exception);
String result = tryResult.fold(
value -> "Success: " + value,
error -> "Error: " + error.getMessage()
);
assertThat(result).isEqualTo("Error: Test error");
}
@Test
@DisplayName("Should provide default value on error using fold")
void shouldProvideDefaultValueOnErrorUsingFold() {
Try<Integer> tryResult = Try.of(() -> Integer.parseInt("invalid"));
Integer result = tryResult.fold(
success -> success,
error -> 0
);
assertThat(result).isEqualTo(0);
}
@Test
@DisplayName("Should transform both cases to same type")
void shouldTransformBothCasesToSameType() {
Try<String> successTry = Try.success("hello");
Try<String> failureTry = Try.failure(new IOException("IO error"));
Integer successResult = successTry.fold(
String::length,
error -> -1
);
Integer failureResult = failureTry.fold(
String::length,
error -> -1
);
assertThat(successResult).isEqualTo(5);
assertThat(failureResult).isEqualTo(-1);
}
@Test
@DisplayName("Should throw exception when fold called with null value mapper")
void shouldThrowExceptionWhenFoldCalledWithNullValueMapper() {
Try<String> tryResult = Try.success("hello");
assertThatThrownBy(() -> tryResult.fold(null, error -> "error"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Neither mapper can be null");
}
@Test
@DisplayName("Should throw exception when fold called with null error mapper")
void shouldThrowExceptionWhenFoldCalledWithNullErrorMapper() {
Try<String> tryResult = Try.success("hello");
assertThatThrownBy(() -> tryResult.fold(value -> "success", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Neither mapper can be null");
}
@Test
@DisplayName("Should throw exception when fold called on Void type")
void shouldThrowExceptionWhenFoldCalledOnVoidType() {
Try<Void> tryResult = Try.success();
assertThatThrownBy(() -> tryResult.fold(
value -> "success",
error -> "error"
))
.isInstanceOf(IllegalStateException.class)
.hasMessage("A fold operation cannot be performed when the try type is Void");
}
@Test
@DisplayName("Should handle complex transformations in fold")
void shouldHandleComplexTransformationsInFold() {
Try<List<Integer>> tryResult = Try.of(() -> List.of(1, 2, 3, 4, 5));
String result = tryResult.fold(
list -> "Sum: " + list.stream().mapToInt(Integer::intValue).sum(),
error -> "Could not calculate sum: " + error.getMessage()
);
assertThat(result).isEqualTo("Sum: 15");
}
@Test
@DisplayName("Should handle exception types in error mapper")
void shouldHandleExceptionTypesInErrorMapper() {
Try<Integer> tryResult = Try.of(() -> Integer.parseInt("not a number"));
String result = tryResult.fold(
value -> "Parsed: " + value,
error -> {
if (error instanceof NumberFormatException) {
return "Invalid number format";
} else {
return "Unknown error: " + error.getMessage();
}
}
);
assertThat(result).isEqualTo("Invalid number format");
}
@Test
@DisplayName("Should preserve original exception information in fold")
void shouldPreserveOriginalExceptionInformationInFold() {
RuntimeException originalException = new RuntimeException("Original message");
Try<String> tryResult = Try.failure(originalException);
Throwable result = tryResult.fold(
value -> new IllegalStateException("Should not happen"),
error -> error
);
assertThat(result).isSameAs(originalException);
assertThat(result.getMessage()).isEqualTo("Original message");
}
}
@Nested
@DisplayName("Sequence Tests")
class SequenceTests {