5

I have the following problem with hierarchy of traits in Scala code:

First of all, I have a basic trait MyTrait[A] with such definition:

trait MyTrait[A] {
  def v1: A
}

It is then followed by a definition of a trait Base with a type-member:

trait Base[A] {
  type T <: MyTrait[A]
  val baseV: T
}

And, at last, a trait Gen which overrides Base's type member.

trait Gen[A, X <: MyTrait[A]] extends Base[A] {
  type T = X
}

The problem is that in the Gen trait it seems that bounds of the type-member are lost. This can be proven by following tests:

Compiles:

trait Test1 {
  val x: Base[_]
  println(x.baseV.v1)
}

Doesn't compile (value v1 is not a member of Test2.this.x.T):

trait Test2 {
  val x: Gen[_, _]
  println(x.baseV.v1)
}

I would like to know whether it's a limitation of the language or there is a workaround it. Questions on similar topics on stackowerflow (1, 2) appear to be focusing on different aspects than mine and I am genuinely at a loss because I can't find much information about such behavior in Scala.

Scala code template of this question can be found on scastie

a7emenov
  • 162
  • 8
  • Why not defining baseV type as MyTrait[A] directly? – Mahmoud Hanafy Dec 20 '18 at 21:08
  • @MahmoudHanafy, the real piece of code I'm facing is more complicated and involves business logic that is not relevant to the question. The presented code is essentially what I'm having difficulties with. – a7emenov Dec 20 '18 at 21:15
  • Looks a little bit like an xy-problem. Are you sure that you cannot get rid of `Gen[A, X <: MyTrait[A]]`? Wouldn't it be better, instead of a type declaration it a trait, to simply provide a constructor method `gen[A, X <: MyTrait[A]]: Base[A] { type T = X }` somewhere? – Andrey Tyukin Dec 20 '18 at 21:17
  • @AndreyTyukin, this is actually interesting in terms of dealing with the issue and I'll definitely play around with the idea. I'm still curious, however, why a wildcard type doesn't satisfy the required bounds. – a7emenov Dec 20 '18 at 21:31

1 Answers1

5

This works:

trait Test2 {
  val x: Gen[A, X] forSome { type A; type X <: MyTrait[A] }
  println(x.baseV.v1)
}

I believe the issue is that

Gen[_, _]

Has to mean

Gen[_ >: Nothing <: Any, _ >: Nothing <: Any]

Which is the same as

Gen[A, X] forSome { type A; type X }

That is, even though the bounds on Gen say that X <: MyTrait[A], the wildcards do not inherit that bound. You can see a similar problem here:

trait Data { def data: String }
trait Box[A <: Data] { def data: A }
def unbox(b: Box[_]): String = b.data.data // nope; the wildcard is not <: Data

We can add the bounds to the wildcards explicitly. However, because the bound on the second wildcard depends on the first one, we are forced to use the extended forSome syntax for the existential, so we can name A and use it twice.

Gen[A, _ <: MyTrait[A]] forSome { type A }

And I opted to just put everything in the existential clause, which is equivalent:

Gen[A, X] forSome { type A; type X <: MyTrait[A] }

You can also use

Gen[_, _ <: MyTrait[_]]

but this is not equivalent, as it doesn't relate the left and right parameters. If Gen[A, _] contained an A in addition to a MyTrait[A], then using x: Gen[_, _ <: MyTrait[_]] would render the "bare" value and the "wrapped" value with incompatible types.

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • Yes, to be honest, I've tried the approach similar to your last line of code before asking the question, but I wondered if there is a better way. What also puzzles me - how can `Test2` compile without `println` if `T` (formally) doesn't satisfy the type bounds? Do you happen to know whether there is a specification I can read about such specifics? – a7emenov Dec 20 '18 at 21:11
  • [This bit](https://www.scala-lang.org/files/archive/spec/2.12/03-types.html#parameterized-types) says that type parameters must conform to the bounds, but your broken `Test2` gives non-conforming type parameters to `Gen`. The specification doesn't seem to be very specific, but I'll hazard a guess that even though `Gen[A, X]` when `X <\: MyTrait[A]` is not well formed, the existential `Gen[A, X] forSome { type A; type X; }` is. I think the intuition is that non-conforming parameters correspond to an empty type, and the existential just unions the empty type with the well-formed "subtype". – HTNW Dec 20 '18 at 21:20