For clarity, this line of thought originally arose from questions around outcome aggregation and failure representation in the context of Loom’s StructuredTaskScope, particularly when reasoning about multiple concurrent subtasks and their combined results. The sketch above is not intended to suggest changes to Loom or to structured concurrency APIs, but simply reflects how thinking through that specific context led me to explore more general value-oriented outcome modeling as a way to reason about the trade-offs discussed.

I’m not proposing this as a solution, nor suggesting that such an abstraction should exist; this was simply a way for me to think more concretely about the design space and the trade-offs discussed in the thread. If nothing else, I found the exercise useful for clarifying where value-oriented outcomes align well with Java’s existing idioms, and where they introduce tension. I’ll defer to the architects’ judgment on whether any of this is worth revisiting now or in the future, and appreciate the perspectives shared in helping me better understand the constraints involved.

Note: I am content with these */not/*being extensible by mere mortals, but maintained and extended through the exclusive diligence of the Java Language Architects.

Even when using value-oriented outcomes such as Result, callers must still be prepared for exceptions to be thrown, whether due to programming errors, contract violations, cancellation, or JVM-level conditions. This sketch is not intended to eliminate or replace Java’s exception model, but to complement it where expected failure can be modeled explicitly as data.

   package java.util;

   import java.util.function.Function;

   public sealed interface Result<V, F>
            permits Result.Success, Result.Failure {

        // --- Variants ---

        record Success<V, F>(V value) implements Result<V, F> {}
        record Failure<V, F>(F failure) implements Result<V, F> {}

        // --- Constructors ---

        static <V, F> Result<V, F> success(V value) {
            return new Success<>(value);
        }

        static <V, F> Result<V, F> failure(F failure) {
            return new Failure<>(failure);
        }

        // --- Introspection ---

        default boolean isSuccess() {
            return this instanceof Success<?, ?>;
        }

        default boolean isFailure() {
            return this instanceof Failure<?, ?>;
        }

        // --- Core transforms ---

        default <U> Result<U, F> map(Function<? super V, ? extends U>
   mapper) {
            return switch (this) {
                case Success<V, F>(var value) ->
                        Result.success(mapper.apply(value));
                case Failure<V, F>(var failure) ->
                        Result.failure(failure);
            };
        }

        default <G> Result<V, G> mapFailure(Function<? super F, ?
   extends G> mapper) {
            return switch (this) {
                case Success<V, F>(var value) ->
                        Result.success(value);
                case Failure<V, F>(var failure) ->
                        Result.failure(mapper.apply(failure));
            };
        }

        default <U> Result<U, F> flatMap(
                Function<? super V, ? extends Result<U, F>> mapper) {
            return switch (this) {
                case Success<V, F>(var value) ->
                        mapper.apply(value);
                case Failure<V, F>(var failure) ->
                        Result.failure(failure);
            };
        }

        // --- Extraction ---

        default V orElse(V fallback) {
            return switch (this) {
                case Success<V, F>(var value) -> value;
                case Failure<V, F>(var failure) -> fallback;
            };
        }

        default V orElseGet(Function<? super F, ? extends V> fallback) {
            return switch (this) {
                case Success<V, F>(var value) -> value;
                case Failure<V, F>(var failure) -> fallback.apply(failure);
            };
        }

        default <X extends Throwable> V orElseThrow(
                Function<? super F, ? extends X> toException) throws X {
            return switch (this) {
                case Success<V, F>(var value) -> value;
                case Failure<V, F>(var failure) ->
                        throw toException.apply(failure);
            };
        }

        // --- Optional interop ---

        static <T, F> Result<T, F> fromOptional(Optional<T> optional, F
   failureIfEmpty) {
            return optional
                    .<Result<T, F>>map(Result::success)
                    .orElseGet(() -> Result.failure(failureIfEmpty));
        }

        static <T, F> Result<T, F> requirePresent(
                Result<Optional<T>, F> result,
                F failureIfEmpty) {

            return result.flatMap(opt ->
                    opt.<Result<T, F>>map(Result::success)
                       .orElseGet(() -> Result.failure(failureIfEmpty)));
        }

        // --- Exception bridge ---

        @FunctionalInterface
        interface ThrowingSupplier<T> {
            T get() throws Throwable;
        }

        static <T> Result<T, Throwable> catching(ThrowingSupplier<T>
   supplier) {
            try {
                return Result.success(supplier.get());
            } catch (Throwable t) {
                return Result.failure(t);
            }
        }

        // --- Lossy projections ---

        default Optional<V> success() {
            return switch (this) {
                case Success<V, F>(var value) ->
   Optional.ofNullable(value);
                case Failure<V, F>(var failure) -> Optional.empty();
            };
        }

        default Optional<F> failure() {
            return switch (this) {
                case Success<V, F>(var value) -> Optional.empty();
                case Failure<V, F>(var failure) ->
   Optional.ofNullable(failure);
            };
        }
   }

example

   switch (result) {
        case Result.Success(var value)   -> use(value);
        case Result.Failure(var failure) -> handle(failure);
   }

Notes:

 * The name java.util.Result may collide with the existing
   java.xml.transform.Result (formerly javax.xml.transform.Result).
   This is not a hard conflict, but it does introduce potential import
   ambiguity and would need to be weighed.
 * The example switch uses record patterns and pattern matching for
   switch, which assumes availability of the relevant language features
   (currently preview / release-dependent).
 * Null handling is unspecified. As written, both Success(null) and
   Failure(null) are possible, and the lossy projections (success() /
   failure()) would treat null as absence. A real API would likely want
   to either forbid nulls or explicitly document this behavior.
 * The instance methods success() and failure() (lossy projections to
   Optional) share names with the static factory methods success(V) /
   failure(E). This is legal but may be a source of confusion and could
   merit different naming.
 * The catching helper currently captures all Throwable, including
   Error. This is intentional for discussion, but in a real API it
   would likely raise questions about whether to catch Exception only,
   or to offer separate variants.
 * The ThrowingSupplier interface is included as a convenience
   placeholder. The JDK may or may not want to introduce an additional
   throwing functional interface in java.util.
 * Providing mapFailure naturally raises questions about whether
   complementary recovery helpers (e.g., recover, recoverWith) should
   exist. They are intentionally omitted here to keep the surface area
   minimal.
 * The Optional interop helpers (fromOptional, requirePresent) are
   intended to clarify the distinction between “operation failed” and
   “operation succeeded but value is absent,” rather than to suggest a
   particular modeling style.

Cheers, Eric

Reply via email to