Result.java
package com.guinetik.corefun;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* A functional container representing either a success or failure outcome.
* <p>
* {@code Result} is an Either-style monad that encapsulates either a successful value
* or an error, providing a functional approach to error handling without exceptions.
* This eliminates the need for null checks and exception handling in business logic,
* promoting cleaner, more composable code.
* </p>
*
* <h2>Key Features</h2>
* <ul>
* <li><b>Type-safe error handling</b> - Errors are part of the type signature</li>
* <li><b>Functional composition</b> - Chain operations with {@link #map} and {@link #flatMap}</li>
* <li><b>Pattern matching</b> - Use {@link #fold} to handle both cases uniformly</li>
* <li><b>Validation support</b> - Combine with {@link #validate} for input validation</li>
* <li><b>Recovery</b> - Handle failures gracefully with {@link #recover}</li>
* </ul>
*
* <h2>Example Usage</h2>
* <pre class="language-java"><code>
* Result<User, String> result = findUser(id);
*
* // Pattern matching with fold
* String message = result.fold(
* error -> "Error: " + error,
* user -> "Found: " + user.getName()
* );
*
* // Chaining operations
* Result<String, String> greeting = result
* .map(user -> user.getName())
* .map(name -> "Hello, " + name);
*
* // Validation
* Result<Integer, String> validAge = Result.success(age)
* .validate(a -> a >= 0 && a <= 150, a -> "Invalid age: " + a);
*
* // Recovery from errors
* Result<User, String> userOrGuest = findUser(id)
* .recover(error -> Result.success(guestUser));
* </code></pre>
*
* @param <S> the type of the success value
* @param <F> the type of the failure value
* @author Guinetik <guinetik@gmail.com>
* @since 0.1.0
* @see Try
* @see SafeCallable#toResult()
*/
public abstract class Result<S, F> {
// Private constructor to prevent external subclassing
Result() {}
/**
* Creates a successful Result containing the given value.
*
* @param value the success value
* @param <S> success type
* @param <F> failure type
* @return a successful Result
*/
public static <S, F> Result<S, F> success(S value) {
return new Success<>(value);
}
/**
* Creates a failed Result containing the given error.
*
* @param error the failure value
* @param <S> success type
* @param <F> failure type
* @return a failed Result
*/
public static <S, F> Result<S, F> failure(F error) {
return new Failure<>(error);
}
/**
* Returns true if this Result represents a success.
*
* @return true if success, false if failure
*/
public abstract boolean isSuccess();
/**
* Returns true if this Result represents a failure.
*
* @return true if failure, false if success
*/
public abstract boolean isFailure();
/**
* Gets the success value. Throws if this is a failure.
*
* @return the success value
* @throws IllegalStateException if this is a failure
*/
public abstract S get();
/**
* Gets the failure value. Throws if this is a success.
*
* @return the failure value
* @throws IllegalStateException if this is a success
*/
public abstract F getError();
/**
* Applies one of two functions depending on the Result state.
* This is the primary way to extract values from a Result.
*
* <p>Example:</p>
* <pre class="language-java"><code>
* String message = result.fold(
* error -> "Failed: " + error,
* value -> "Success: " + value
* );
* </code></pre>
*
* @param <R> the return type
* @param onFailure function to apply if failure
* @param onSuccess function to apply if success
* @return the result of applying the appropriate function
*/
public abstract <R> R fold(Function<? super F, ? extends R> onFailure,
Function<? super S, ? extends R> onSuccess);
/**
* Transforms the success value using the provided function.
* If this is a failure, returns a new failure with the same error.
*
* @param <T> the new success type
* @param mapper function to transform the success value
* @return a new Result with the transformed value
*/
public abstract <T> Result<T, F> map(Function<? super S, ? extends T> mapper);
/**
* Transforms the success value with a function that returns a Result.
* Useful for chaining operations that might fail.
*
* <p>Example:</p>
* <pre class="language-java"><code>
* Result<User, String> user = findUser(id);
* Result<Account, String> account = user.flatMap(u -> findAccount(u.getAccountId()));
* </code></pre>
*
* @param <T> the new success type
* @param mapper function that returns a Result
* @return the Result from the mapper, or a failure
*/
public abstract <T> Result<T, F> flatMap(Function<? super S, Result<T, F>> mapper);
/**
* Transforms the failure value using the provided function.
*
* @param <E> the new failure type
* @param mapper function to transform the failure value
* @return a new Result with the transformed failure
*/
public abstract <E> Result<S, E> mapFailure(Function<? super F, ? extends E> mapper);
/**
* Returns the success value or a default if this is a failure.
*
* @param defaultValue the default to return on failure
* @return success value or default
*/
public abstract S getOrElse(S defaultValue);
/**
* Returns the success value or computes a default if this is a failure.
*
* @param supplier supplies the default value
* @return success value or computed default
*/
public abstract S getOrElseGet(Supplier<? extends S> supplier);
/**
* Recovers from a failure by applying a function that returns a new Result.
*
* <p>Example:</p>
* <pre class="language-java"><code>
* Result<User, String> user = findUser(id)
* .recover(error -> findGuestUser());
* </code></pre>
*
* @param handler function to handle the failure
* @return this if success, or the result of the handler
*/
public abstract Result<S, F> recover(Function<? super F, Result<S, F>> handler);
/**
* Validates the success value against a predicate.
* If validation fails, produces a failure using the provided function.
*
* @param predicate condition to test
* @param failureProducer produces failure value if predicate fails
* @return this if valid, or a failure
*/
public abstract Result<S, F> validate(Function<? super S, Boolean> predicate,
Function<? super S, ? extends F> failureProducer);
/**
* Performs an action on the value (success or failure) without altering the Result.
*
* @param observer consumer that receives the value
* @return this Result unchanged
*/
public abstract Result<S, F> peek(Consumer<Object> observer);
/**
* Performs an action on the success value if present.
*
* @param action consumer for the success value
* @return this Result unchanged
*/
public abstract Result<S, F> peekSuccess(Consumer<? super S> action);
/**
* Performs an action on the failure value if present.
*
* @param action consumer for the failure value
* @return this Result unchanged
*/
public abstract Result<S, F> peekFailure(Consumer<? super F> action);
/**
* Converts a list of Results into a Result of a list.
* Returns the first failure encountered, or a success containing all values.
*
* <p>Example:</p>
* <pre class="language-java"><code>
* List<Result<Integer, String>> results = List.of(
* Result.success(1),
* Result.success(2),
* Result.success(3)
* );
* Result<List<Integer>, String> combined = Result.sequence(results);
* // Success[[1, 2, 3]]
*
* List<Result<Integer, String>> withFailure = List.of(
* Result.success(1),
* Result.failure("oops"),
* Result.success(3)
* );
* Result<List<Integer>, String> failed = Result.sequence(withFailure);
* // Failure[oops]
* </code></pre>
*
* @param <S> the success type
* @param <F> the failure type
* @param results the list of Results to sequence
* @return a Result containing all success values, or the first failure
*/
public static <S, F> Result<List<S>, F> sequence(List<Result<S, F>> results) {
List<S> values = new ArrayList<>(results.size());
for (Result<S, F> result : results) {
if (result.isFailure()) {
return Result.failure(result.getError());
}
values.add(result.get());
}
return Result.success(values);
}
/**
* Applies a function to each element and sequences the results.
* This is equivalent to mapping each element to a Result and then sequencing.
*
* <p>Example:</p>
* <pre class="language-java"><code>
* List<String> inputs = List.of("1", "2", "3");
* Result<List<Integer>, String> parsed = Result.traverse(inputs, s -> {
* try {
* return Result.success(Integer.parseInt(s));
* } catch (NumberFormatException e) {
* return Result.failure("Invalid number: " + s);
* }
* });
* // Success[[1, 2, 3]]
*
* List<String> mixed = List.of("1", "bad", "3");
* Result<List<Integer>, String> failed = Result.traverse(mixed, s -> ...);
* // Failure[Invalid number: bad]
* </code></pre>
*
* @param <T> the input element type
* @param <S> the success type
* @param <F> the failure type
* @param items the list of items to traverse
* @param mapper function that converts each item to a Result
* @return a Result containing all success values, or the first failure
*/
public static <T, S, F> Result<List<S>, F> traverse(List<T> items, Function<? super T, Result<S, F>> mapper) {
List<S> values = new ArrayList<>(items.size());
for (T item : items) {
Result<S, F> result = mapper.apply(item);
if (result.isFailure()) {
return Result.failure(result.getError());
}
values.add(result.get());
}
return Result.success(values);
}
// --- Success implementation ---
private static final class Success<S, F> extends Result<S, F> {
private final S value;
Success(S value) {
this.value = value;
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public boolean isFailure() {
return false;
}
@Override
public S get() {
return value;
}
@Override
public F getError() {
throw new IllegalStateException("Cannot get error from a success Result");
}
@Override
public <R> R fold(Function<? super F, ? extends R> onFailure,
Function<? super S, ? extends R> onSuccess) {
return onSuccess.apply(value);
}
@Override
public <T> Result<T, F> map(Function<? super S, ? extends T> mapper) {
return Result.success(mapper.apply(value));
}
@Override
public <T> Result<T, F> flatMap(Function<? super S, Result<T, F>> mapper) {
return mapper.apply(value);
}
@Override
@SuppressWarnings("unchecked")
public <E> Result<S, E> mapFailure(Function<? super F, ? extends E> mapper) {
return (Result<S, E>) this;
}
@Override
public S getOrElse(S defaultValue) {
return value;
}
@Override
public S getOrElseGet(Supplier<? extends S> supplier) {
return value;
}
@Override
public Result<S, F> recover(Function<? super F, Result<S, F>> handler) {
return this;
}
@Override
public Result<S, F> validate(Function<? super S, Boolean> predicate,
Function<? super S, ? extends F> failureProducer) {
if (!predicate.apply(value)) {
return Result.failure(failureProducer.apply(value));
}
return this;
}
@Override
public Result<S, F> peek(Consumer<Object> observer) {
observer.accept(value);
return this;
}
@Override
public Result<S, F> peekSuccess(Consumer<? super S> action) {
action.accept(value);
return this;
}
@Override
public Result<S, F> peekFailure(Consumer<? super F> action) {
return this;
}
@Override
public String toString() {
return "Success[" + value + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Success)) return false;
Success<?, ?> other = (Success<?, ?>) obj;
return value == null ? other.value == null : value.equals(other.value);
}
@Override
public int hashCode() {
return value == null ? 0 : value.hashCode();
}
}
// --- Failure implementation ---
private static final class Failure<S, F> extends Result<S, F> {
private final F error;
Failure(F error) {
this.error = error;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public boolean isFailure() {
return true;
}
@Override
public S get() {
throw new IllegalStateException("Cannot get value from a failure Result: " + error);
}
@Override
public F getError() {
return error;
}
@Override
public <R> R fold(Function<? super F, ? extends R> onFailure,
Function<? super S, ? extends R> onSuccess) {
return onFailure.apply(error);
}
@Override
@SuppressWarnings("unchecked")
public <T> Result<T, F> map(Function<? super S, ? extends T> mapper) {
return (Result<T, F>) this;
}
@Override
@SuppressWarnings("unchecked")
public <T> Result<T, F> flatMap(Function<? super S, Result<T, F>> mapper) {
return (Result<T, F>) this;
}
@Override
public <E> Result<S, E> mapFailure(Function<? super F, ? extends E> mapper) {
return Result.failure(mapper.apply(error));
}
@Override
public S getOrElse(S defaultValue) {
return defaultValue;
}
@Override
public S getOrElseGet(Supplier<? extends S> supplier) {
return supplier.get();
}
@Override
public Result<S, F> recover(Function<? super F, Result<S, F>> handler) {
return handler.apply(error);
}
@Override
public Result<S, F> validate(Function<? super S, Boolean> predicate,
Function<? super S, ? extends F> failureProducer) {
return this;
}
@Override
public Result<S, F> peek(Consumer<Object> observer) {
observer.accept(error);
return this;
}
@Override
public Result<S, F> peekSuccess(Consumer<? super S> action) {
return this;
}
@Override
public Result<S, F> peekFailure(Consumer<? super F> action) {
action.accept(error);
return this;
}
@Override
public String toString() {
return "Failure[" + error + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Failure)) return false;
Failure<?, ?> other = (Failure<?, ?>) obj;
return error == null ? other.error == null : error.equals(other.error);
}
@Override
public int hashCode() {
return error == null ? 0 : error.hashCode();
}
}
}