Dependent Class Types for Scala
See original GitHub issueDependent Class Types for Scala
Motivation
Scala already supports many variants of dependent types (aka. types that depend on terms), e.g.
- path-dependent types
- dependent method types
- dependent function types
- singleton types
- literal types
The combination of higher-kinded types and singleton types is another commonly used approach to encode dependent types in Scala:
trait Context {
type Tree
}
class Helper[T <: Context](val ctx: T) {
def f(x: Int): ctx.Tree = ???
}
val ctx : Context = ???
val tree : ctx.Tree = new Helper[ctx.type](ctx).f(5)
However, the encoding above is tedious in syntax. Instead, most programmers would expect the following code to work:
trait Context {
type Tree
}
class Helper(val ctx: Context) {
def f(x: Int): ctx.Tree = ???
}
val ctx : Context = ???
val tree : ctx.Tree = new Helper(ctx).f(5)
Compile the code above in Scala 2.12.4 will result in the following error message:
found : _1.ctx.Tree where val _1: Helper
required: Test.ctx.Tree
val tree : ctx.Tree = new Helper(ctx).f(5)
The fact that classes in Scala are ignorant of referential equality also
limits the expressive power of dependent function types. For example, given
the code below, currently it’s impossible to implement the function f
:
class Animal(name: String)
trait Foo[T]
class Bar(val a: Animal) extends Foo[a.type]
val f : (a: Animal) => Foo[a.type] = ???
If we try to implement f
with (a: Animal) => new Bar(a)
, the Dotty compiler will
generate following error message:
7 | val f : (a: Animal) => Foo[a.type] = (a: Animal) => new Bar(a)
| ^^^^^
| found: Bar
| required: Foo[Animal(a)]
But the definition of Bar
says that any instance of Bar(a)
is a subtype of Foo[a.type]
!
Related issues: https://github.com/scala/bug/issues/5712, https://github.com/scala/bug/issues/5700, #1262.
Proposal
To address the problems above as well as make dependent types more useful, we
propose dependent class types. We introduce an annotation @dependent
that can be
used to mark a public field of in a class constructor depenent:
class Animal(name: String)
trait Foo[T]
class Bar(@dependent val a: Animal) extends Foo[a.type]
The compiler should produce a dependent refinement type for dependent class instantiation as follows:
val dog : Dog = new Animal("dog")
val bar : Bar { val a: dog.type @dependent } = new Bar(dog)
The subtype checking should allow the following:
// Bar { val a: dog.type @dependent } <: Foo[dog.type]
val foo : Foo[dog.type] = bar
Rules
-
@dependent
can only be used in class primary constructor to decorate non-private, non-mutable, non-lazy, non-overloaded fields. -
A depenent refinement type
M { val x: T @dependent }
is valid ifx
is a non-private, non-mutable, non-lazy, non-overloaded field ofM
, andT
is a subtype ofM.member(x)
. -
If a primary constructor parameter
x
of a classBar
is annotated with@dependent
, the return type of the constructor is refined asBar { val x: x.type @dependent }
. Thex
inx.type
refers the constructor parameter, thus the constructor has a dependent method type. -
When getting the base type of a valid dependent refinement type
M { val x: T @dependent }
for a parent classP
, first get the base typeB
frombaseType(M, P)
, then substitute all references toM.member(x)
with the typeT
.
Implementation
We don’t need syntax change to the language, nor introduce new types in the compiler.
- We need to introduce an annotation
@dependent
.
package scala.annotation
final class dependent extends StaticAnnotation
-
We need add checks for valid usage of
@dependent
in class definitions and dependent refinement types. The checks are better to be done in RefChecks, as they are semantic. -
We need to update the type checking for constructors, to refine the result type with the dependent refinement
{ val x: x.type @dependent }
if any constructor parameterx
is annotated with@dependent
. The change in consideration looks like the following:
val oldRetType = ...
val refinedRetType = termParamss.foldLeft(oldRetType) { (acc, termParams) =>
termParams.foldLeft(acc) { (acc, termParam) =>
if (acc.hasDependentAnnotation)
RefinedType(acc, termParam.name, termParam.termRef)
else acc
}
}
- We need to change base type computation to handle dependent refinements. The change in consideration looks like the following:
case tp @ RefinedType(parent, name, refine) =>
val res = baseTypeOf(tp.superType)
if (tp.isDependentRefinement)
res.subst(tp.nonPrivateMember(name).symbol :: Nil, refine :: Nil)
else res
Application
Dependent class types can implement ML-like functor modules:
import scala.annotation.dependent
trait Ordering {
type T
def compare(t1:T, t2: T): Int
}
class SetFunctor(@dependent val ord: Ordering) {
type Set = List[ord.T]
def empty: Set = Nil
implicit class helper(s: Set) {
def add(x: ord.T): Set = x :: remove(x)
def remove(x: ord.T): Set = s.filter(e => ord.compare(x, e) != 0)
def member(x: ord.T): Boolean = s.exists(e => ord.compare(x, e) == 0)
}
}
object Test {
val orderInt = new Ordering {
type T = Int
def compare(t1: T, t2: T): Int = t1 - t2
}
val IntSet = new SetFunctor(orderInt)
import IntSet._
def main(args: Array[String]) = {
val set = IntSet.empty.add(6).add(8).add(23)
assert(!set.member(7))
assert(set.member(8))
}
}
Change Logs
- Changed
@dep
to@dependent
, thanks @propensive . - Added related issues, thanks @julienrf .
- Add application about functor modules
Issue Analytics
- State:
- Created 6 years ago
- Reactions:1
- Comments:24 (19 by maintainers)
Top GitHub Comments
The following code (which compiles and runs with #3936 ) shows that dependent class types can implement ML-like functor modules:
I think the use case is very important, though I’ve never been an advocate of more annotations for Scala, so I’d prefer a solution which avoids them…
…however, if it is the best way, could you use
@dependent
instead? Using@dep
saves only a few characters on a (likely) rarely-used annotation, but comes at the cost of some clarity for people who encounter it for the first time.