Description
The Current State
The current Dotty implementation allows to leave out the parent of an anonymous class if it is known from the expected type. So the following compiles
trait C { type T; val m: T }
def f(x: Int): C = new { type T = Int; val m = x }
This avoids duplication since we would have to write otherwise
def f(x: Int): C = new C { type T = Int; val m = x }
This matters if the name C
is long or there are type parameters.
Likewise, the following also compiles:
def g(x: C) = ???
def test = g(new { type T = String; val m = "" })
That's a bit more dubious since the name C
does not appear anywhere in the call, so this looks like a pure structural new
that uses reflection for dispatch.
None of this is currently specified except for a single line in syntax.md:
SimpleExpr ::= ... | ‘new’ TemplateBody
A Possible Alternative: Structural Instances
- Use
C with { ... }
as an anonymous class - Allow
C with { ... }
as the result type of adef
without having to write a RHS, mimicking what we do for givens.
This means the first example above would be written like this:
trait C { type T; val m: T }
def f(x: Int): C = C with { type T = Int; val m = x }
which can be abbreviated to
def f(x: Int): C with { type T = Int; val m = x }
In fact the abbreviated form is more useful than the original forms since the result of f
is known to bind T
to Int
.
There was an explanation about this in #10538. Quoting here:
This has another interesting consequence. Taken by itself, can we give a meaning to
T with { defs }
? In fact, this does make sense as an alternative syntax for an anonymous class. If we go down that path, we can truly get rid of all vestiges ofnew
in the syntax (over time, not for 3.0).
Taking this further, we can now see the following (approximate) equivalence:
given x: C with { ... } ~~~ given x: C = C with { ... } ~~~ given x: C = new C {...}
So,
: C with {...}
is approximately: C = C with {...}
, and it avoids the repetition. In fact: C with {}
is more useful than: C = C with {...}
since it keeps any refinements in{...}
in the type. So it's more like a RHS= new {...}
and an inferred result type. In fact it's even better than that, since an anonymous classnew { ... }
is subject to avoidance, but: C with { .. }
creates the class alongside the given instead of in its right hand side. This means that members freshly introduced in{...}
are visible in the result of the given. To see the difference, consider this code:
class C
given c: C with
def foo = 1
given d: C = new C { def foo = 1 }
def test =
c.foo // OK
d.foo // error: `foo` is not a member of `d`.
Now, one intriguing step further is whether we want to allow the same convention for normal defs. I.e
def foo(x: Int): T with {...}
as a slightly more powerful alternative to
def foo(x: Int) = T with {...}
or, using
new
def foo(x: Int) = new T {...}
with the same expansion as for the
given
. This would give us parameterized objects, or functors in the SML sense, without having to define a class. It also gives a nice way to avoid duplication between return type and RHS while stile having an explicitly defined return type.
What to do?
We should not adopt both new {...}
and structural instances. I believe structural instances are ultimately a lot more powerful and useful than new {...}
, in particular since they give us the power of object
(which can define new members not covered in the extends clause) with parameters.
So our choices are:
- Spec
new {}
for 3.0 and close the door for structural instances. - Adopt structural instances for 3.0, including spec and implementation. We might be too late for this.
- Drop
new {}
, keeping the door open for structural instances later. - Same as (3), except we put
new {}
under an experimental flag for now.