Proposal: generic receiver parameter
Brian Goetz
brian.goetz at oracle.com
Mon Apr 4 22:06:54 UTC 2022
This is a familiar request, which often goes under the name of "self
types". It shows up most commonly in hierarchies of builders, but can
also show up in other "fluent" APIs. (This feature is somewhat related
to another commonly requested feature (also about chaining), which is to
allow "void" methods to be chained as if they "return this".)
The addition of explicit receivers in 8 (which was introduced at the
time to allow receiver annotations, but we always knew that it might
find use in type refinement as well) offers another option for how to
express this. Effectively, the only thing such a method can return under
these constraints is `this` (well, also null), since it has no other
way(s) to produce a witness to T.
This observation:
> However, if the fluent method has multiple type parameters, then its
> invocations that explicitly supply type arguments should be allowed to
> omit the receiver type argument; so the receiver type parameter must
> be declared either first or last.
is what pushes this particular approach to exposing it over the edge;
this is the sort of feature interaction where a small feature has a
non-orthogonal effect on a bigger feature (making some sort of type
parameters more "special" than others), which we try very hard to
avoid. There's way too high a chance that were we to do this now (which
feels like a "small irregularity" when you're focused on what you want
to accomplish now), that my successor would curse me for it for what it
forecloses on.
Overall, the notion of "self types" or "this return" is a "mostly
harmless" feature, whose intrusion is relatively limited, but not one
that we've historically placed a very high priority on. They both
mainly exist to support chaining, and chaining is simply not that
incrementally expressive to justify adding extra language features to
squeeze a little extra value out of it.
On 4/3/2022 12:31 PM, Mohamed Maaliki wrote:
> Hello. I've designed multiple fluent APIs with inheritance. While on
> one hand their usage can be concise and readable, on the other their
> implementation is verbose.
>
> Let's say that we have
> ```
> class A {
> A withX(int x) {...}
> }
> class B extends A {
> B withY(int y) {...}
> }
> ```
> . `new B().withX(x).withY(y)` is erroneous because `A::withX` returns
> `A`. I see 2 solutions to this problem:
> 1. overriding `withX` with return type `B`; and
> 2. adding a "`this` type" parameter `T extends A<T>` to `A`.
> Solution #1 is acceptable in small classes (which are unlikely) that
> don't change but if `A` receives new methods or is large, then the
> implementation of `B` becomes very noisy.
> Solution #2 reduces noise in `B`'s implementation but it makes usage
> of `A` and its subclasses verbose instead: instantiation needs a
> diamond (`new B<>().withX(x).withY(y)`); field and parameter
> declarations of type `B` have to redundantly specify a wildcard type
> parameter (`B<?> parameter`); and such fluent methods must perform an
> unsafe cast to `T` when returning.
> This API that was initially intended to be readable now hurts
> readability.
>
> Since the JLS allows the receiver to be declared explicitly, the
> problem could be solved more elegantly by extending it to allow the
> receiver to be generic:
> ```
> class A {
> <T extends A> T withX(T this, int x) {...}
> }
> class B extends A {
> <T extends B> T withY(T this, int y) {...}
> }
> ```
> . The compiler substitutes the type of the receiver for `T`. Now `B`
> can extend `A` without a redundant and potentially unsafe type
> parameter or having to override `withX` and their usages are clean:
> ```
> void useB(B b) {
> doSomething(b.withX(x).withY(y));
> }
> ```
> . We can see that `T` is always assignable to its enclosing type; so
> we could eliminate the explicit upper bound: `<T> T withX(T this, int
> x) {...}`.
>
> However, if the fluent method has multiple type parameters, then its
> invocations that explicitly supply type arguments should be allowed to
> omit the receiver type argument; so the receiver type parameter must
> be declared either first or last.
>
> Regards.
>
More information about the amber-dev
mailing list