-
Couldn't load subscription status.
- Fork 1.1k
Description
Dependent 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: scala/bug#5712, scala/bug#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] = barRules
-
@dependentcan 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 ifxis a
non-private, non-mutable, non-lazy, non-overloaded field ofM, and
Tis a subtype ofM.member(x). -
If a primary constructor parameter
xof a classBaris annotated with
@dependent, the return type of the constructor is refined asBar { val x: x.type @dependent }. Thexinx.typerefers 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 typeBfrom
baseType(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
@dependentin 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 parameterxis 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 resApplication
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
@depto@dependent, thanks @propensive . - Added related issues, thanks @julienrf .
- Add application about functor modules