In Scala 3 (dotty) or Scala 2, how to make dependent types transitive?

96 Views Asked by At

Here is a simple example:

  { // dependent type in function
    def dep[B](a: Any, bs: Seq[B]): Seq[(a.type, B)] = {

      val result: Seq[(a.type, B)] = bs.map { b =>
        (a: a.type) -> (b: B)
      }
      result
    }

    val ss = dep(3, Seq("a"))

    val ss2: Seq[(3, String)] = ss
  }

It works because a.type automatically resolves to 3.type at call site, despite that a doesn't even have a deterministic path at definition site.

Works fine so far, but with a little twist, the call site expansion will no longer work:

  { // in case class
    class Dep[B](a: Any, bs: Seq[B]) {
      def result: Seq[(a.type, Any)] = {

        val result: Seq[(a.type, B)] = bs.map { b =>
          (a: a.type) -> (b: B)
        }
        result
      }
    }

    object ss extends Dep(3, Seq("a"))

    val ss2: Seq[(3, String)] = ss.result
  }

/*
Found:    Seq[((ss.a : Any), Any)]
Required: Seq[((3 : Int), String)]

Explanation
===========

Tree: ss.result
I tried to show that
  Seq[((ss.a : Any), Any)]
conforms to
  Seq[((3 : Int), String)]
but the comparison trace ended with `false`:
*/

Since Dep is now a class constructor, the call site will stick to its member type definition instead of call site type. This caused a lot of confusion and violation of constructor function principles, Is there a way to augment the compiler to unify these 2 cases?

The closest I could come up with is to use an auxiliary constructor, but it only generates some inscrutable compiling error:

  { // in case class, as auxiliary constructor
    case class Dep[A, B](a: A, bs: Seq[B]) {

      def this[B](a: Any, bs: Seq[B]) = this[a.type, B](a, bs)

      def result: Seq[(A, Any)] = {

        val result: Seq[(A, B)] = bs.map { b =>
          (a: A) -> (b: B)
        }
        result
      }
    }
  }

/*
None of the overloaded alternatives of constructor Dep in class Dep with types
 [A, B](): Dep[A, B]
 [A, B](a: A, bs: Seq[B]): Dep[A, B]
match arguments (Null)
*/
2

There are 2 best solutions below

3
Andrey Tyukin On BEST ANSWER

Here is a suggestion that avoids all the trouble with constructors our auxiliary types A for a:

  abstract class Dep[B](bs: Seq[B]) {
    val a: Any
    def result: Seq[(a.type, B)] = bs.map(a -> _)
  }
  
  object Obj extends Dep(Seq("a")):
    val a: 3 = 3
  
  val s: Seq[(3, String)] = Obj.result

The point is that it doesn't let any values disappear into / resurface from any calls (method calls or constructor calls): it just defines a completely static a on a completely static singleton object.

I've found that the same pattern scales just fine to much more complex Obj-definitions with lots of subcomponents, which in turn have many more dependent member types that are much more complicated that the singleton .type. Additionally, it provides a good place for the compiler to inline all the macros defined in Dep1 ... DepN into the Obj (something that doesn't work with constructors at all, because you cannot pass macros into constructors).

5
Dima On

I don't think auxiliary constructors can have their own type parameters (or construct an instance of a different type). Another problem is that even if you could define that constructor, that would make it ambiguous (new Dep(1, Seq("foo")) matches both).

For the same (latter) reason apply doesn't work too. The best I could think of, is just make another "factory method":

case class Dep[A, B](a: A, bs: Seq[B]) {
 def result = bs.map { a -> _ }
}

object Dep { 
  def narrow[B](a: Any, bs: Seq[B]): Dep[a.type, B] = new Dep(a, bs)
}

Dep.narrow(3, Seq("foo", "bar")).result

If you are willing to swap the parameters around case class Dep[A, B](bs: Seq[B], a: A) or add an extra dummy param to resolve the ambiguity, then you could just use apply:

case class Dep[A, B](a: A, bs: Seq[B], dummy: Int) {
 def result = bs.map { a -> _ }
}

object Dep { 
  def apply[B](a: Any, bs: Seq[B]): Dep[a.type, B] = new Dep(a, bs, 0)
}

Dep(3, Seq("foo", "bar")).result

But if you are just looking to construct a specialized object, then this works:

object ss extends Dep[3, String](3, Seq("foo"))
val ss2: Seq[(3, String)] = ss.result