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