Open odersky opened 4 years ago
You can't type this
as This
unless you prove in every class and trait (not just final classes) that This
does not collapse to Nothing
. The main thing that self types provides is not to restrict the type of this
(that's a side benefit you get); the main thing is that it prevents illegal extensions. So in
trait Foo { this: Bar => }
trait Bar
trait Foobar extends Foo
error: illegal inheritance;
self-type Foobar does not conform to Foo's selftype Foo with Bar
the last line is already illegal and flagged with a compile error, because the self type in Foobar
is not compatible with the one in Foo
. In this example, with This
types a similar error would be reported (although more obscure if most of those This
es are compiler-generated):
trait Foo { type This <: Foo with Bar }
trait Bar { type This <: Bar }
trait Foobar extends Foo { type This <: Foobar }
error: incompatible type in overriding
type This <: Foo with Bar (defined in trait Foo);
found : <: Foobar
required: <: Foo with Bar
But would that always be the case? I can't convince myself, but maybe it's true.
The clone()
example is unsafe. It assumes that clone()
will always be implemented on top of Object.clone()
which actually provides the same class, but clone()
can be overridden to return something else.
It's only safe if you know what you are doing and you have a sealed hierarchy (or you have final
ized clone()
). But in that case you can also implement your own This
thing (it might be verbose, but it's possible).
But would that always be the case? I can't convince myself, but maybe it's true.
It's enforced in the same way in Dotty, at least:
|trait Foobar extends Foo { type This <: Foobar }
| ^
| error overriding type This in trait Foo with bounds <: Foo & Bar;
| type This with bounds <: Foobar has incompatible type
There is one restriction we have to add: In a new C
, the type of This
is also implicitly
set to C
. So new C
is treated the same as new C { }
for the purposes of This
checking. new C{}
in turn expands to new C{ type This = C }
. So if a base class has another idea of what This
is this would give an error. That's how we prevent illegal extensions.
Before we can properly evaluate alternatives, we need to come up with an exhaustive list of all the semantics of self types, for example they influence the implicit scope:
trait Foo {
implicit def x: Int = 1
}
trait Bar { self: Foo =>
println(implicitly[Int])
}
we need to come up with an exhaustive list of all the semantics of self types
Do you have some reliable way of compiling such a list?
This is another case where I would feel much safer if we evolved the language gradually, instead of introducing so many changes at once.
At this point there is a big limit to what people are using Dotty for. OTOH if we would ship a final release with some subset of features, and "scala.next" would always have a limited set of changes relative to "scala.current," you would have much more projects trying out the changes in "scala.next."
IMO the only changes that should be in 3.0 are (a) changes that form the identity of 3.0, and (2) changes whose design affects other changes in 3.0 (so that if you implement it later you may regret a change that was already released in light of the newer change). Theoretically, if you would make a graph of all the changes in Dotty right now, with arrows indicating how the design of one change can be influenced by the design or reception of other changes, each disconnected subgraph could be in its own release. I understand that would make keeping documentation and training materials up to date harder but that has to be the cart that follows the horse of low-risk changesets.
Do you have some reliable way of compiling such a list?
The self-type is stored in the field self
of the Template
tree, so I'd start by doing a find all references on that.
Theoretically, if you would make a graph of all the changes in Dotty right now, with arrows indicating how the design of one change can be influenced by the design or reception of other changes,
http://dotty.epfl.ch/docs/reference/features-classification.html is an attempt at doing something like that, though not fully up-to-date
The only legal form of such a declaration is with an upper bound: type
This <: B
.
This
is already used as a type name, in most cases in a role quite similar to the meaning ofThis
defined here. The rule thatThis
may only have upper bounds will probably detect almost all existing uses ofThis
that are now illegal.
To me, these scream for a solution where what's refined is not a fictitious This
type, but a this
value:
class C { this: T => ... }
would be replaced by
class C { val this: T; ... }
This has several advantages:
no need for a weird special rule (i.e., the "only upper bounds" rule), although the fact that this
cannot be given an explicit value could be seen as one;
backwards compatible: no name clashes with existing code;
can be desugared to the old self-type encoding internally.
So we've achieved the stated goal: remove the vey strange { self: T => ... }
syntax (I rememebr being bogged down by that syntax when I was learning, as in "is this class declaring a lambda in its body?!") in favor of something whose meaning is much more straightforward.
The downside is that it doesn't give us a way to talk about "the current class" in operations like copy
, but as @sjrd said it's generally unsafe anyways, and in the situations where it's safe we can simply use the actual type (like is done for case class copy
methods).
Just saw this. I think this could be interesting and desirable (I've seen people try to get this by using this.type
), but what do we know about this feature's soundness, formally? Self-types are (a) modeled by DOT (b) sound, but requiring lots of care.
In fact, just like for recursive types and object creation, in DOT you'd have to restrict ThisType
to have tight bounds, or accept val obj = new { type This >: Any <: Nothing }
. These don't seem needed for Dotty, but I haven't seen people have a convincing explanation for this.
(OTOH, it's not clear that this feature is especially worrisome).
The clone
example is orthogonal because it includes a cast, so type soundness guarantees don't cover it. However, the only extra power shown here requires unsafe casts.
Also, how does this compare to ThisType from Kim Bruce or from Sukyoung Ryu (TOPLAS 2016, https://dl.acm.org/citation.cfm?id=2888392)? (Yes, I should take a look, no time now).
Non-paywalled link to the Sukyoung Ryu paper here.
A related thread on contributors: https://contributors.scala-lang.org/t/proposal-abstract-this-type-inside-every-class-trait/2607
Self types declarations are a weird little corner case of the language. They achieve two things:
this
to a proper subtype of the current classthis
.Self type declarations have non-obvious syntax (somebody not well versed in the Scala language wouldn't know what a self type declaration is) and add considerable complexity to the compiler.
(2) can already be achieved by giving a simple alias like
or
So the real addition to expressiveness that self types provide is (1). But in my mind a better way to achieve (1) is to introduce a
This
type, with the following rules:Every class or trait
C
has an implicitThis
type declaration.C
is final, the declaration istype This = C
type This <: C
o
, the declaration istype This = o.type
.The type of
this
isThis
.This
types may also be declared explicitly.type This <: B
.This
type declaration. I.e. in classC
, an explicit declarationtype This <: B
would yieldtype This <: B & C
as the final type ofThis
.Once
This
types are introduced, self type declarations are redundant and can be dropped. A definition likewould be replaced by
Motivation
This
types are more powerful than self types. In particular they allow to accurately type copying operations:This
types are also more regular than self types since they need no special syntax.Problems
The main problem lies with migration.
This
is already used as a type name, in most cases in a role quite similar to the meaning ofThis
defined here. The rule thatThis
may only have upper bounds will probably detect almost all existing uses ofThis
that are now illegal. A simple mitigation is to pick another name for existing usages (e.g.ThisType
, orSelf
). But is it feasible to do it automatically?