Scala getting the concrete type of a self-type

912 Views Asked by At

I'd like to get a reference to the concrete type of a self-type annotation in Scala within the self-typed trait. I have something like this:

trait Foo
class FooImpl1 extends Foo
class FooImpl2 extends Foo

trait SpecialFoo {
  this:Foo =>

  def |+|(that:this.type):this.type // the type signature is incorrect here
}

where if I do new FooImpl1 with SpecialFoo, I'd like the |+| method to require and return a FooImpl1 (or a subtype of FooImpl1). However, with the above code it seems to want a SpecialFoo.this.type, which is unsurprising, but not what I want.

1

There are 1 best solutions below

0
On

this.type is the singleton type of whatever instance of SpecialFoo you have. As defined, |+| would only be able to be called with itself. For example:

trait Spec { def |+|(that: this.type): this.type = that }
val s = new Spec {}
val s2 = new Spec {}

scala> s |+| s
res1: <refinement>.type = $anon$1@118102ee

scala> s |+| s2
<console>:14: error: type mismatch;
 found   : Spec
 required: <refinement>.type
       s |+| s2
             ^

this.type is FooImpl1 in some cases, but the compiler has no way of knowing that. You need some way to capture the more refined type of FooImpl1 or FooImpl2. The self-type this: Foo => only cares that it's a Foo. There are a couple possibilities, but neither will look as nice as you want.

You can parameterize SpecialFoo:

trait Foo
class FooImpl1 extends Foo
class FooImpl2 extends Foo

trait SpecialFoo[A <: Foo] { self: A =>
  def |+|(that: A): A
}

val foo = new FooImpl1 with SpecialFoo[FooImpl1] {
  def |+|(that: FooImpl1): FooImpl1 = that
}

Unforunately, you need to write FooImpl1 twice, but the self-type still prevents you from mixing two different implementations.

The alternative is to use type members within Foo. You wouldn't have to specify the implementation type twice when creating a SpecialFoo, but would when creating the implementations themselves to bind the correct types.

trait Foo { type S }
class FooImpl1 extends Foo { type S = FooImpl1 }
class FooImpl2 extends Foo { type S = FooImpl2 }

trait SpecialFoo { self: Foo =>
  def |+|(that: self.S): self.S
}

val foo = new FooImpl1 with SpecialFoo {
  def |+|(that: FooImpl1): FooImpl1 = that
}

You could also make Foo F-bounded, i.e. trait Foo[A <: Foo], and do something similar to the above example.