Surprising equivalences and non-equivalences regarding this.type
Asked Answered
M

1

6

It appears to make a difference whether you refer to this.type from inside a Trait or from the scope where the object is created, with surprising results.

import scala.reflect.runtime.universe._

trait Trait {
  val ttag = typeOf[this.type]
  println(s"Trait constructor: $this")
}

object Instance1 extends Trait

object Instance2 extends Trait

println(typeOf[Instance1.type] =:= typeOf[Instance2.type])  // Should be false
println(Instance1.ttag =:= Instance2.ttag)                  // Should be false
println(Instance1.ttag =:= typeOf[Instance1.type])          // Should be true

Here's the output:

false    // As expected: the singleton types of two objects are different.
Trait constructor: $line9.$read$$iw$$iw$$iw$$iw$Instance1$@58c46295
Trait constructor: $line10.$read$$iw$$iw$$iw$$iw$Instance2$@452451ba
true     // But the this.type tags are equivalent
false    // and the this.type tag is not equivalent to the singleton type.

So, there are two distinct objects, but apparently each is getting an equivalent type tag for its this.type, which is not equivalent to the .type of the object as seen from an enclosing scope.

Is this a compiler bug, or, if not, could you explain why this behavior makes sense?

(I'm running Scala 2.11.2. I've tried it with a self alias for this, with the same result.)

Milord answered 18/8, 2015 at 22:3 Comment(3)
For what it's worth, when I run your code I get an internal compiler error. So, a compiler bug doesn't seem too unlikely. As another way to experiment, a more direct way to get at a similar idea is to have a method that returns the type of a local object.Otiliaotina
@Otiliaotina Based on our conversation below, my search through the spec, and a search through the issues database, I went ahead and posted my first Scala bug. I hope it's not a duplicate or actually not a bug!Milord
My current understanding (building from Owen's and Jason Zaugg's comments): each path-dependent type has a “prefix” telling where to find the object within its scope. Type equivalence-checking looks at the prefix, not the object itself (which exists only at run-time and might never get created). So, inside Trait, this.type has prefix Trait, resulting in type Trait.this.type—for all instances. Instance1.type has a prefix saying that the object is defined in the top-level scope, with name Instance1, resulting in type Instance1.type—different than inside Trait.Milord
O
2

The following program prints false and then true. To my mind, there should be no material difference between these two cases (though that's really more of an opinion; I can't really say if there's a reason or not):

import scala.reflect.runtime.universe._

object Test2 extends App {
  def foo(): Type = {
    object a
    typeOf[a.type]
  }

  println(foo() =:= foo()) // false

  trait Trait {
    val ttag = typeOf[this.type]
  }

  object Instance1 extends Trait

  object Instance2 extends Trait

  println(Instance1.ttag =:= Instance2.ttag) // true
}
Otiliaotina answered 18/8, 2015 at 23:46 Comment(8)
I think that since the two objects created by the calls to foo are different, it's correct that their singleton-type type tags are different. Creating an object violates referential transparency—or does it? Maybe this is the key to what I’m asking about.Milord
The spec says that an object definition defines a single object of a new class. So far, I haven't found anything to indicate whether a single object definition inside a function definition should define one object or as many objects as times the function is called. But it does appear that the two Instances, having separate definitions, should have separate types.Milord
@BenKovitz Passing -Yprint:typer could be informative here... it will desugar object definitions into a class definition and a new instance creation, so you can see exactly how many instances are created.Otiliaotina
Thanks for the suggestion. I've been playing a bit with -Xprint:typer and -Ytyper-debug. (Note slightly different flags, in case it matters.) Here's my current hazy guess at what's going on: Since a is in scope, the TypeTag macro makes a SingleType that refers to a. But inside the Trait, all we get is a ThisType that knows only the trait, not the object.Milord
I'm still not too clear on the difference between a SingleType, SingletonType, and ThisType, though. But anyway, your suggestion of putting the object in a local scope might be just what I need to make a workaround for what I'm actually doing. More experimentation to come…Milord
Thinking about this some more, I still don't get it. Both results from foo are SingleType(NoPrefix, TermName("a")). So how can =:= tell them apart?Milord
@BenKovitz I don't know how it works under the hood. It seems to me that each singleton type foo.type where foo is a stable identifier is uniquely identified at runtime by a tuple of the type path of foo and the identity of the actual object foo.Otiliaotina
Well, that might be the central question: Is a singleton type defined at compile-time or run-time? Hypothesis du jour: it's at compile-time for objects defined in a static scope (like inside an object), and it's at run-time for objects defined in a dynamic scope (like inside a function) but there's still a compile-time signature that enables type-checking. That's only a guess, though. I haven't found it discussed in the spec, though it could be in there somewhere.Milord

© 2022 - 2024 — McMap. All rights reserved.