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