scaladependent-typetype-systemsscala-3dotty

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


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)
*/

Solution

  • 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).