0
votes

The following fails to compile with Scala 2.11.4:

trait Test {
  type A
  type F = Function1[A, String]
}

trait Util[T <: Test] {
  def compute1( f: T#A => String, a: T#A ): String = f(a)
  def compute2( f: T#F, a: T#A ): String = f(a)
  //                                         ^
}

There is a compile error on the argument of the call (^):

type mismatch; 
found : a.type (with underlying type T#A) required: _3317.A

I was expecting that the two f arguments in compute1 and compute2 would have the same type; apparently not.

What's going on?

As @Eugene and @Zim-Zam replied, the problem here is due to Scala's path-dependent types. Based on their suggestions, I came up with several alternatives:

trait Test {
  type A
  type Fa = Function1[A, String]       // original question
  type Fb = Function1[Test#A, String]  // Zim-Zam's suggestion
}

trait TestOps[T <: Test] {
  type G = Function1[T#A, String]
}

trait Util[T <: Test] {
  def compute1( f: T#A => String, a: T#A ): String = f(a)
  // def compute2a( f: T#Fa, a: T#A ): String = f(a)
  // type mismatch; found : a.type (with underlying type T#A) required: _1536.A
  def compute2b( f: T#Fb, a: T#A ): String = f(a)
}

trait Util1 {  
  def compute3a(t: Test)( f: t.Fa, a: t.A ): String = f(a)
  def compute3b(t: Test)( f: t.Fb, a: t.A ): String = f(a)
}

trait Util2[T <: Test] { tops: TestOps[T] =>
  // def compute4a( f: T#Fa, a: T#A ): String = f(a)
  // type mismatch; found : a.type (with underlying type T#A) required: _1642.A
  def compute4b( f: T#Fb, a: T#A ): String = f(a)
  def compute5( f: tops.G, a: T#A ): String = f(a)
}

Util compares the original operation with @Zim-Zam's suggestion

Util1 exercises the difference in specifying the Function1 argument type: A vs Test#A

Util2 exercises the suggestion to define the Function1 type to another trait: TestOps

Commenting out the variations that don't type check, we're left with:

compute1 compute2b compute3a compute3b compute4b compute5

Which is better for specializing these traits?

To tease out the differences, I made a simple refinement:

class U 

class TestU extends Test {
  override type A = U  
}

class UOps extends TestOps[TestU]

Here are the results:

trait UtilU extends Util[TestU] {
  def get1( f: TestU#A => String, a: TestU#A) = compute1(f, a)
  // def get2b( f: TestU#A => String, a: TestU#A) = compute2b(f, a)
  // type mismatch; found : A.U ⇒ String required: A.Test#A ⇒ String
}

trait UtilU1 extends Util1 {
  val u = new TestU()
  def get3a( f: u.A => String, a: u.A) = compute3a(u)(f, a)
  // def get3b( f: u.A => String, a: u.A) = compute3b(u)(f, a)
  // type mismatch; 
  // found : UtilU1.this.u.A ⇒ String (which expands to) A.U ⇒ String 
  // required: UtilU1.this.u.Fb (which expands to) A.Test#A ⇒ String
}

class UtilU2 extends Util2[TestU] with TestOps[TestU] {
  // def get4b( f: TestU#A => String, a: TestU#A) = compute4b(f, a)
  // type mismatch; found : A.U ⇒ String required: A.Test#A ⇒ String
  def get5( f: TestU#A => String, a: TestU#A) = compute5(f, a)
}

So, we're left with only 3 reasonable variations:

compute1 (no Function1 type alias) compute3a compute5

For aliasing Function1 types, we're left with just 2 alternatives: compute3a and compute5

What are the proper ways to describe the differences between them?

2

2 Answers

4
votes

It's because of the way how path-dependent types implemented in scala

val test1 = new Test {}
val test2 = new Test {}
// test1.A is different type from test2.A

in your example you are basically saying that f and a can be passed from different instances of Test, and in this case A used in type F in first argument will be different A from second argument.

But if you'll restrict it to some instance of T it will compile

def compute2(t: T)( f: t.F, a: t.A ): String = f(a)

Upd However it still should be the case for compute1

3
votes

@Eugene is correct about this being caused by path dependent types - you can fix it with

trait Test {
  type A
  type F = Function1[Test#A, String]
}

Now F can take any A as its argument