-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Dependent Class Types for Scala #3920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
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) You can avoid having to write the type parameter by hand at the use-site by having a Singleton upper bound: class Helper[T <: Context with Singleton](val ctx: T) {
def f(x: Int): ctx.Tree = ???
}
val ctx : Context = ???
val tree : ctx.Tree = new Helper(ctx).f(5) |
I wholeheartedly support this idea that would save me a lot of lines of code. The workaround based on upper bounded type parameters is inconvenient (see also #1262 where I give more details about that). I don’t understand why we need an annotation for that, though. Why couldn’t that be the default behavior? Maybe related issues in scala: scala/bug#5712 and scala/bug#5700 |
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 |
@julienrf Having it as the default behaviour will generate a lot of refinement types in the compiler, which slows down (sub-)type checking. It's too expensive for a feature that's not widely used. Also, as @smarter pointed out in #1262, it causes user inconvenience in other usages: val foo1 = new Foo { ... }
val foo2 = new Foo { ... }
var x = new Quux(foo1) // x is inferred to have type `Quux { val foo: foo1.type }`
x = new Quux(foo2) // error: foo2.type does match foo1.type
// To solve this, write "var x: Quux = ..." |
Not sure how exactly this would work out, but could you utilize the |
Reuse keyword for a language feature would make the keyword mysterious, IMHO. In types, we could remove |
Well, I think in the case of Though if It would be like, "you can use this feature, but if you do, you lose the ability to change the value and its type in a subclass". It doesn't seem so bad to avoid the intersection of two subtle features by making them into just one. |
I see your point @propensive , we can consider it if there is known incompatibility of this proposal and subclass/overidding. In absence of known major incompatibilities, it seems difficult to justify the design decision from the theoretical perspective. The justification from ergonomics and worry about potential uncertain issues is not strong enough. |
Yes, I'm not 100% sure it's right solution, but I would have said exactly the same about the use of |
Cool that you’re looking into this! Here and elsewhere, it looks to me like it’s often hard to use certain features of the Scala core—Scala supports many forms of abstractions from ML modules (and it is designed to do so), they’re just hard to use.
I’m not happy about overloading |
What if we had a trait Context {
type Tree
} the user write: dependent class Helper(val ctx: Context) {
def f(x: Int): ctx.Tree = ???
} and we generate: class Helper(val ctx: Context) {
def f(x: Int): ctx.Tree = ???
}
object Helper {
def apply(ctx0: Context): Helper { val ctx: ctx0.type } = {
class Helper0(ctx: ctx0.type) extends Helper(ctx)
new Helper0(ctx0)
}
} Then the following code would compile: object Test {
val ctx : Context = ???
val tree : ctx.Tree = Helper(ctx).f(5)
} |
We could also avoid having to create a subclass by using a cast in the implementation: def apply(ctx0: Context): Helper { val ctx: ctx0.type } = {
new Helper(ctx0).asInstanceOf[Helper { val ctx: ctx0.type }]
} |
When I use dependent types with val members in traits (as opposed to constructor parameters of classes), yes, I find it sometimes useful to narrow the type of the val. Here is an example from https://github.com/julienrf/endpoints. |
@Blaisorblade
that will work if Also, I'm not sure I got your question and/or point at the end about overloading the meaning of I sort of see it as a few separate questions:
|
The following code (which compiles and runs with #3936 ) shows that 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))
}
} |
@smarter Making // Bar { val a: dog.type @dependent } <: Foo[dog.type]
val foo : Foo[dog.type] = bar |
@liufengyun That’s cool! I’d love to see how much can be conveniently encoded, including sharing constraints and so on. I’d like to try encoding the examples from my Scala’16 talk, and look for better ones from the literature. |
From a discussion with @julienrf , I'm convinced that his proposal in #1262 makes sense and it is a better proposal. His proposal overlaps with this one, but it's more coherent with the language/compiler, and it's practical enough to solve real world problems. Dependent class types can become tricky with subtyping and correct compiler implementation, and its theoretical foundation is unknown. I'd like to keep it as a separate research instead of proposing it as a language feature. Thus I'm closing the issue for now. We can continue research discussions offline or by email. |
@liufengyun @julienrf Right now both #1262 and this proposal are closed. Is the plan to proceed on #1262 or to do more research on this? EDIT: or to... keep thinking about the problem without any open issue to track it? |
I actually really like the proposal said here #3920 (comment) with the caveat that I agree with @Blaisorblade here though, both tickets are now closed which implies that none of the approaches are being considered? |
The problem with this proposal is that it doesn't work with trait parameters, thus this proposal is proposing an incoherent language feature. Currently abstract member of trait or class will get a refinement type: trait Animal
class Dog extends Animal
class Cat extends Animal
abstract class Bar { val x: Animal }
val bar: Bar { val x: Cat } = new Bar { val x = new Cat }
trait Foo { val x: Animal }
val foo: Foo { val x: Cat } = new Foo { val x = new Cat } As convincingly argued by @julienrf , there should be a symmetry if the members are in the constructor positions: abstract class Bar(val x: Animal)
val bar: Bar { val x: Cat } = new Bar(new Cat) {} // error
trait Foo(val x: Animal)
val foo: Foo { val x: Cat } = new Foo(new Cat) {} // error If such symmetry exists, it will be sufficient for most important usage in practice, thus makes this proposal not so useful. |
@liufengyun Maybe we should reopen the other ticket then? |
Actually my initial proposal is not applicable because it would cause too much type refinements. We have to improve it. I think the direction proposed in this proposal is very nice, it is not clear to me why it can not be applied to trait parameters? |
@julienrf I was wrong, it could work with traits, but still it introduces an unfriendly syntax for an important language feature, it doesn't seem to be an elegant design. Instead, during a discussion about this in the morning meeting, people tend to agree with your argument that the symmetry of type checking behaviour is well-founded, and we should try to do that for |
Dependent Class Types for Scala
Motivation
Scala already supports many variants of dependent types (aka. types that depend on terms), e.g.
The combination of higher-kinded types and singleton types is another
commonly used approach to encode dependent types in Scala:
However, the encoding above is tedious in syntax. Instead, most programmers would
expect the following code to work:
Compile the code above in Scala 2.12.4 will result in the following error message:
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
:If we try to implement
f
with(a: Animal) => new Bar(a)
, the Dotty compiler willgenerate following error message:
But the definition of
Bar
says that any instance ofBar(a)
is a subtype ofFoo[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 beused to mark a public field of in a class constructor depenent:
The compiler should produce a dependent refinement type for dependent class instantiation as follows:
The subtype checking should allow the following:
Rules
@dependent
can only be used in class primary constructor to decoratenon-private, non-mutable, non-lazy, non-overloaded fields.
A depenent refinement type
M { val x: T @dependent }
is valid ifx
is anon-private, non-mutable, non-lazy, non-overloaded field of
M
, 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 constructorhas 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 thetype
T
.Implementation
We don't need syntax change to the language, nor introduce new types in the compiler.
@dependent
.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:Application
Dependent class types can implement ML-like functor modules:
Change Logs
@dep
to@dependent
, thanks @propensive .The text was updated successfully, but these errors were encountered: