scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.8k stars 1.05k forks source link

Introduce `This` type to replace self types #7374

Open odersky opened 4 years ago

odersky commented 4 years ago

Self types declarations are a weird little corner case of the language. They achieve two things:

  1. Restrict the type of this to a proper subtype of the current class
  2. Introduce an alias name for this.

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

val self = this

or

val self: this.type = this

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:

Once This types are introduced, self type declarations are redundant and can be dropped. A definition like

class C { this: T => ... }

would be replaced by

class C { type This <: T; ... }

Motivation

This types are more powerful than self types. In particular they allow to accurately type copying operations:

def copy: This = clone().asInstanceOf[This]
def withField(fld: T): This =
  if this.fld == fld
    this 
  else 
    val c = copy
    c.fld = fld
    c

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 of This defined here. The rule that This may only have upper bounds will probably detect almost all existing uses of This that are now illegal. A simple mitigation is to pick another name for existing usages (e.g. ThisType, or Self). But is it feasible to do it automatically?

sjrd commented 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 Thises 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.

sjrd commented 4 years ago

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 finalized clone()). But in that case you can also implement your own This thing (it might be verbose, but it's possible).

odersky commented 4 years ago

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.

smarter commented 4 years ago

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])
}
nafg commented 4 years ago

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.

smarter commented 4 years ago

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

LPTK commented 4 years ago

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 of This defined here. The rule that This may only have upper bounds will probably detect almost all existing uses of This 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:

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).

Blaisorblade commented 4 years ago

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).

milessabin commented 4 years ago

Non-paywalled link to the Sukyoung Ryu paper here.

soronpo commented 4 years ago

A related thread on contributors: https://contributors.scala-lang.org/t/proposal-abstract-this-type-inside-every-class-trait/2607