3

Consider the following simple example involving Aux-pattern:

sealed trait AdtBase

abstract case class Foo(){
  type T <: AdtBase
}

object Foo{
  type Aux[TT] = Foo { type T = TT }
}

abstract case class Bar(){
  type T <: AdtBase
  val foo: Foo.Aux[T]
}

object Bar {
  type Aux[TT] = Bar { type T = TT }

  def apply[TT <: AdtBase](f: Foo.Aux[TT]): Bar = new Bar() {
    override type T = TT
    override val foo: Foo.Aux[T] = f
  }
}

case class Baz(foo: Foo)

def testBaz(baz: Baz) = Bar(baz.foo) //Compiles fine
def testFoo(foo: Foo) = Bar(foo) //Error: Type mismatch

Scastie

I don't really understand why testBaz compiles. I expected type mismatch as well.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Some Name
  • 8,555
  • 5
  • 27
  • 77
  • Why do you expect type mismatch? For `trait A[T]` and `def m[T](a: A[T]) = ???` existential application `m(??? : A[_])` compiles. And `Foo =:= Foo.Aux[_]`. – Dmytro Mitin Nov 04 '20 at 02:31
  • @DmytroMitin Maybe I missed the point, but don't `baz.foo` and `foo` have the same type? At least I thought the bahavior should have been the same. – Some Name Nov 04 '20 at 02:39
  • Well, if in some case a type is inferred and in another a type isn't inferred then one compiles, another doesn't. – Dmytro Mitin Nov 04 '20 at 02:46
  • On contrary to what I wrote in the first comment `trait A { type T }` `def m[_T](a: A { type T = _T}) = ???` `m(??? : A)` doesn't compile. So there is difference in type inference for a type parameter and type member. – Dmytro Mitin Nov 04 '20 at 02:47
  • https://stackoverflow.com/questions/59148665/whats-different-between-def-applytct-and-type-tdef-applyct – Dmytro Mitin Nov 04 '20 at 02:51
  • https://stackoverflow.com/questions/62873718/scala-case-class-copy-doesnt-always-work-with-existential-type/ – Dmytro Mitin Nov 04 '20 at 02:59
  • 3
    I would rather ask why `testFoo` doesn't compile. If you give a little push it does: https://scastie.scala-lang.org/LjOH4tKXRvGFvw8sTCIvDg . Dotty doesn't seem to suffer from this: https://scastie.scala-lang.org/VaBHR9JlSwSK8vUYnHFH1g which sounds right as there resolution of path dependent types got improved. – Mateusz Kubuszok Nov 04 '20 at 11:56
  • @MateuszKubuszok Don't you know if such behavior is consistent with a specification of scala 2.13? – Some Name Nov 04 '20 at 13:08
  • 1
    AFAIK a similar behavior was a limitation of type system/inference which lead to development of Aux pattern in the first place. Ideally both cases should work and in Dotty they do work, and Aux pattern should not be needed anymore. I guess it's the right behavior for Scala 2 but I cannot point you to the right place in a specification. – Mateusz Kubuszok Nov 04 '20 at 13:45
  • 1
    @MateuszKubuszok `Aux` pattern is necessary in Dotty much rarer than in Scala 2 because there are parameters of path-dependent types with prefixes depending on parameters from the same parameter list, multiple implicit parameter lists and interchanging implicit and ordinary parameter lists. But in some cases `Aux`-types are still necessary: `def foo(using tc1: TC1[tc2.Out], tc2: TC2.Aux[tc1.Out]) = ???` doesn't compile while `def bar[A, B](using tc1: TC1.Aux[A, B], tc2: TC2.Aux[B, A]) = ???` and `def baz[A](using tc1: TC1[A], tc2: TC2.Aux[tc1.Out, A]) = ???` do. – Dmytro Mitin Nov 05 '20 at 23:22
  • 1
    @MateuszKubuszok Also it's just more convenient to write `TC.Aux[A, B]` than `TC[A] { type Out = B }` if you want to specify type refinement precisely (e.g. in return type). – Dmytro Mitin Nov 05 '20 at 23:22
  • 2
    @SomeName Type inference is mostly not specified. – Dmytro Mitin Nov 05 '20 at 23:25
  • @DmytroMitin My original use-case of Aux pattern is to provide a way to restrict a method signature to accept arguments of type `case class A1(...) { type T }` and `case class A2(...) { type T }` with the same `type T`. – Some Name Nov 06 '20 at 12:44
  • @MateuszKubuszok There was a typo in my comment. I meant `def foo(using tc1: TC1[tc2.Out], tc2: TC2[tc1.Out]) = ???`. – Dmytro Mitin Nov 07 '20 at 15:04

1 Answers1

3

It seems there is no deep reason for that.

Since when you specify type parameter explicitly both methods compile

def testBaz(baz: Baz) = Bar[baz.foo.T](baz.foo) //compiles
def testFoo(foo: Foo) = Bar[foo.T](foo)         //compiles

it seems in

def testBaz(baz: Baz) = Bar(baz.foo) //compiles
//def testFoo(foo: Foo) = Bar(foo)   //doesn't compile

in the first case the type baz.foo.T is inferred while in the second case the type foo.T is just not inferred

// found   : Foo
// required: Foo.Aux[this.T]

In Scala it's always possible that some type parameter will not be inferred and you'll have to specify it explicitly.


Maybe I found a possible reason.

The code

class testFoo2(foo: Foo) {
  // Bar(foo) // doesn't compile
}

doesn't compile but if you make foo a val

class testFoo2(val foo: Foo) {
  Bar(foo) // compiles
}

then it does. Probably the thing is that when foo is a val it's more "stable" and in such case it's "easier" to infer path-dependent type foo.T.

So the difference between testBaz and testFoo can be that Baz is a case class so foo is a val while in testFoo foo is just a method parameter and therefore less "stable".

Similarly , on contrary to

trait A[T]
def m[T](a: A[T]) = ???
m(??? : A[_]) // compiles

the code

trait A { type T } 
def m[_T](a: A { type T = _T}) = ??? 
m(??? : A) // doesn't compile

doesn't compile but if we extract a variable

val a: A = ???
m(a) // compiles

then it does. The thing is that now a is stable and type a.T can be inferred.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • I have a question related to path-dependent type which is not really related to the OP. So there is a `trait Foo` with a `type T <: AdtBase` member. Given `foo: Foo` do we have any information about what `foo.T` is at runtime? – Some Name Nov 07 '20 at 22:24
  • 1
    @SomeName You should start a new question for this. The answer is not one line. The answer is yes and no. – Dmytro Mitin Nov 07 '20 at 22:51