Draft JEP: Enhanced Void return

Stephan Herrmann stephan.herrmann at berlin.de
Thu Mar 27 20:22:13 UTC 2025


How would this interact with definitions void-compatible / value-compatible in 
JLS §15.27.2.?

It seems all lambdas formerly classified as void-compatible would need to be 
re-classified as "potentially value-compatible" with an elided "return null;". 
This could entail a hole new can of worms for resolution / type inference.

BR
Stephan

> Date: Thu, 27 Mar 2025 12:16:18 -0700
> From: Brandon Mintern <mintern at everlaw.com>
> To: compiler-dev at openjdk.org
> Subject: Re: Draft JEP: Enhanced Void return
> Message-ID:
> 	<CAJW1onhzJX6yFnxM94SwpWoMVV1H0a35_1cxQp6B5qNui7ibWw at mail.gmail.com>
> Content-Type: text/plain; charset="utf-8"
> 
> I'm sorry. I see that my prior email wraps poorly when rendered in the web
> interface. Here's the same email wrapped to 76 characters for better
> readability:
> 
> Title: Enhanced Void return
> Author: Brandon Mintern
> Organization: Everlaw
> Created: 2025/03/27
> Type: Feature
> State: Draft
> Exposure: Open
> Component: specification / language
> Scope: SE
> Template: 2.0
> 
> Summary
> -------
> 
> Allow `Void`-returning methods to omit the `return null` statement. Method
> implementations can still `return null` if desired, but they can also use a
> plain `return` or omit the statement altogether.
> 
> Goals
> -----
> 
> Improve ergonomics when using methods that accept a lambda and optionally
> return a result. Enhance the compiler to:
> 
> - Automatically insert `return null` (or convert `return`) in
>    `Void`-returning methods.
> - Infer `Void` as the generic return type in method references and lambdas
>    without a `return`.
> 
> Non-Goals
> ---------
> 
> - `Void`-returning methods are still free to explicitly state `return null`.
> - The scope should be limited such that:
>      - The compiler can do the heavy lifting, without any JVM changes.
>      - No new lambda objects would be constructed implicitly.
> 
> Motivation
> ----------
> 
> Lambdas provide powerful abstractions for working with closeable resources
> or in temporary contexts. For example, we can define the following
> abstraction:
> 
>      <R> R withBufferedReader(Path path, BufferedReaderFunction<R> f) {
>          try (var reader = Files.newBufferedReader(path)) {
>              return f.apply(reader);
>          } catch (IOException e) {
>              throw new UncheckedIOException(e);
>          }
>      }
> 
>      @FunctionalInterface
>      interface BufferedReaderFunction<R> {
>          R apply(BufferedReader reader) throws IOException;
>      }
> 
> Callers can use this method as follows:
> 
>      var firstLine = withBufferedReader(path, r -> r.lines().findFirst());
> 
> When a caller doesn't need the return value, though, the caller must include
> an awkward `return null`:
> 
>      withBufferedReader(path, reader -> {
>          this.header = reader.readLine();
>          this.data = reader.lines().toList();
>          return null;
>      });
> 
> These examples used `BufferedReader` for familiarity, but abstractions like
> this are arguably more useful when reading from other persistent data stores
> (e.g., databases, search indexes) or when writing context-specific code.
> Using a completely hypothetical `UserContextHolder`:
> 
>      <R> R inUserContext(User user, Supplier<R> supplier) {
>          var oldContext = UserContextHolder.getContext();
>          UserContextHolder.setUserContext(user);
>          try {
>              return supplier.get();
>          } finally {
>              UserContextHolder.setContext(oldContext);
>          }
>      }
> 
> Then we might have system code that looks like the following:
> 
>      interface Report {
>          User getUser();
>          GeneratedReport generate();
>      }
> 
>      for (var report: pendingReports) {
>          save(inUserContext(report.getUser(), report::generate));
>      }
> 
>      // or:
> 
>      interface Job {
>          User getUser();
>          void execute();
>      }
> 
>      for (var job : pendingJobs) {
>          inUserContext(job.getUser(), () -> {
>              job.execute();
>              return null;
>          });
>          // inUserContext(job.getUser(), job::execute) with this proposal
>      }
> 
> Description
> -----------
> 
> When a method has a fully instantiated return type of `Void`, all of the
> following would be valid:
> 
> - `return null`
> - `return`
> - omitted `return` (implicit `void` return)
> 
> `Void`-returning methods can continue using `return null` without warning.
> It should be at least a warning, though, for a single method to mix `return
> null` and any `void`-style return (implicit or explicit).
> 
> When a functional interface has a generic return type parameter, lambdas can
> use `void`-style returns, and references to `void`-returning methods can be
> used, where the compiler will infer the return type to be `Void` and
> logically insert the necessary `return null` statements.
> 
> As with functional interfaces in general, an instance of one type cannot be
> automatically converted to another. That is, a `Runnable` cannot be passed
> as a `Supplier<T>`, inferring `T = Void`. `Runnable::run` could be used
> instead.
> 
> When a method returns a generic type, and when that type is inferred as
> `Void` due to the use of an *enhanced `Void` return*, it would be an error
> to assign the result to a variable. Returning to the `inUserContext`
> example:
> 
>      var report = inUserContext(report.getUser(), report::generate); // OK
> 
>      Void result = inUserContext(job.getUser(), () -> {
>          job.execute();
>          return null;
>      }); // OK: result is null (backwards compatible)
> 
>      Void result = inUserContext(job.getUser(), job::execute); // ERROR
> 
>      Void result = inUserContext(job.getUser(), () -> {
>          job.execute();
>      }); // ERROR
> 
> This enhancement would be implemented entirely by the compiler, generating
> JVM bytecode as if `return null` had been used where it's currently needed.
> 
> Alternatives
> ------------
> 
> Returning to the `withBufferedReader` example, without this proposal, we can
> instead define sibling methods (and types) to avoid the need for
> `return null`:
> 
>      void withBufferedReader(Path path, BufferedReaderProcedure f) {
>          withBufferedReader(path, reader -> {
>              f.accept(reader);
>              return null;
>          });
>      }
> 
>      interface BufferedReaderProcedure {
>          void accept(BufferedReader reader) throws IOException;
>      }
> 
>      withBufferedReader(path, reader -> {
>          this.header = reader.readLine();
>          this.data = reader.lines().toList();
>      });
> 
> This works, sort of, but we quickly find that it doesn't actually work.
> Returning to the first example:
> 
>      var firstLine = withBufferedReader(path, r -> r.lines().findFirst());
> 
> This now fails to compile, reporting "Ambiguous method call." We can work
> around that issue, of course, by standardizing on verb pairs to use in these
> scenarios:
> 
>      R applyingBufferedReader(...)
>      void acceptingBufferedReader(...)
> 
> In reality, though, this disambiguation is not the biggest issue. Rather, in
> most codebases, it would be hard to justify defining duplicate methods and
> types. Many development teams would instead use `return null` statements
> where necessary, avoid the abstraction in cases where `return null` is
> needed, or avoid defining the abstraction altogether.
> 
> Risks and Assumptions
> ---------------------
> 
> Some codebases may have sibling methods like those described above, where
> this enhancement would cause new "Ambiguous method call." when using the
> `void`-returning method. However:
> 
> - Already-compiled code would not be affected since there is no JVM change.
> - Impacted code would likely benefit from this enhancement, with new
>    compiler errors pointing to duplicate code that can likely be deleted.
> 
> 


More information about the compiler-dev mailing list