Path-dependent types without a path?
Asked Answered
L

2

5

Consider this trivial example:

class Outer {
  case class Inner()
  def test(i: Inner) = {}
}

As expected, this doesn't compile because of a type mismatch:

val o1 = new Outer()
val o2 = new Outer()

o1.test(o2.Inner())  // doesn't compile

What if we want to define a standalone function?

This is no good

def test[X <: Outer](a: X#Inner, b: X#Inner) = {}

because this compiles as if everything is OK

test(o1.Inner(), o2.Inner())

This works

def test(x: Outer)(a: x.Inner, b: x.Inner) = {}

because this compiles:

test(o1)(o1.Inner(), o1.Inner())

and this doesn't:

test(o1)(o1.Inner(), o2.Inner())

However we have to pass an extra Outer argument to test. Is it possible to avoid this? Ideally, the following should work:

test(o1.Inner(), o1.Inner()) // ok
test(o1.Inner(), o2.Inner()) // compilation error
Lepp answered 16/6, 2017 at 11:31 Comment(0)
D
5

I don't think that, out of the box, it's possible to enforce it in a satisfying way. For instance, one possible solution might be:

scala> def test[X <: Outer#Inner](a: X)(b: X) = ()
test: [X <: Outer#Inner](a: X)(b: X)Unit

scala> test(o1.Inner())(o1.Inner())

scala> test(o1.Inner())(o2.Inner())
<console>:16: error: type mismatch;
 found   : o2.Inner
 required: o1.Inner
       test(o1.Inner())(o2.Inner())
                                ^

Looks good, but you can circumvent it by explicitly passing in the type arguments. (same goes for @OlivierBlanvillain's solution by the way)

scala> test[Outer#Inner](o1.Inner())(o2.Inner())

Now let's try the following:

scala> def test[X <: Outer](a: X#Inner)(b: X#Inner) = ()
test: [X <: Outer](a: X#Inner)(b: X#Inner)Unit

scala> test(o1.Inner())(o2.Inner())

Doesn't work, scalac infers X to be Outer, which isn't specific enough, and we could supply Outer as explicit type argument anyway. We need a way to force X to be a singleton type so that it can only represent the path o1 or o2, and not some general type that can be inhabited by an infinite amount of values. There is a way. Scala has the marker trait Singleton for this purpose. Let's try it:

scala> def test[X <: Outer with Singleton](a: X#Inner)(b: X#Inner) = ()
test: [X <: Outer with Singleton](a: X#Inner)(b: X#Inner)Unit

scala> test(o1.Inner())(o1.Inner())
<console>:15: error: inferred type arguments [Outer] do not conform to method test's type parameter bounds [X <: Outer with Singleton]
       test(o1.Inner())(o1.Inner())
       ^
<console>:15: error: type mismatch;
 found   : o1.Inner
 required: X#Inner
       test(o1.Inner())(o1.Inner())
                    ^

Now our valid case doesn't work anymore! The problem is that scalac refuses to infer singleton types. We have to pass them in explicitly:

scala> test[o1.type](o1.Inner())(o1.Inner())

The invalid cases don't work anymore:

scala> test(o1.Inner())(o2.Inner())
<console>:16: error: inferred type arguments [Outer] do not conform to method test's type parameter bounds [X <: Outer with Singleton]
       test(o1.Inner())(o2.Inner())
       ^
<console>:16: error: type mismatch;
 found   : o1.Inner
 required: X#Inner
       test(o1.Inner())(o2.Inner())
                    ^

scala> test[o1.type](o1.Inner())(o2.Inner())
<console>:16: error: type mismatch;
 found   : o2.Inner
 required: o1.Inner
       test[o1.type](o1.Inner())(o2.Inner())
                                         ^

scala> test[Outer](o1.Inner())(o2.Inner())
<console>:17: error: type arguments [Outer] do not conform to method test's type parameter bounds [X <: Outer with Singleton]
       test[Outer](o1.Inner())(o2.Inner())
           ^

So this enforces the rules we want, but you have to pass in the types explicitly...


EDIT

Actually it turns out you can enforce this without losing type inference and without help from any external libraries, but you're probably not going to like it :-p

META EDIT as pointed out in the comments this can still be circumvented if you try hard enough, so I guess you're stuck with the above solution.

scala> import scala.language.existentials
import scala.language.existentials

scala> def test[X <: x.Inner forSome { val x: Outer }](a: X, b: X) = ()
test: [X <: x.Inner forSome { val x: Outer }](a: X, b: X)Unit

scala> test(o1.Inner(), o1.Inner())

scala> test(o1.Inner(), o2.Inner())
<console>:16: error: inferred type arguments [Outer#Inner] do not conform to method test's type parameter bounds [X <: x.Inner forSome { val x: Outer }]
       test(o1.Inner(), o2.Inner())
       ^
<console>:16: error: type mismatch;
 found   : o1.Inner
 required: X
       test(o1.Inner(), o2.Inner())
                    ^
<console>:16: error: type mismatch;
 found   : o2.Inner
 required: X
       test(o1.Inner(), o2.Inner())
                                ^

scala> test[o1.Inner](o1.Inner(), o2.Inner())
<console>:16: error: type mismatch;
 found   : o2.Inner
 required: o1.Inner
       test[o1.Inner](o1.Inner(), o2.Inner())
                                          ^
Dellora answered 16/6, 2017 at 12:21 Comment(1)
Well beggars can't be choosers :) You still can circumvent this by writing e.g. test[x.Inner forSome { val x: Outer }](o1.Inner(), o2.Inner()) but it's better than nothing.Lepp
L
4

This seems to work:

scala> def t[X <: Outer#Inner, Y <: Outer#Inner](a: X, b: Y)(implicit e: X =:= Y)=1
test: [X <: Outer#Inner, Y <: Outer#Inner](a: X, b: Y)(implicit ev: X =:= Y)Unit

scala> test(o1.Inner(), o1.Inner()) // ok

scala> test(o1.Inner(), o2.Inner())
<console>:15: error: Cannot prove that o1.Inner =:= o2.Inner.
       test(o1.Inner(), o2.Inner())
           ^

You might also want to experiment with two parameter lists as it sometimes affects type inference.

Lignite answered 16/6, 2017 at 11:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.