Lack of context-dependent types #15817
Replies: 3 comments 4 replies
-
Now that the SIP process has been rebooted (https://www.scala-lang.org/blog/2022/07/13/scala-improvement-process-reloaded.html), you might want to open a thread on contributors to discuss this as a pre-SIP for increased visibility: https://contributors.scala-lang.org/c/sip/13 I'm also wondering if it would make sense to have non-contextual parameters, e.g.: opaque type List(dsl: Dsl)[A] =
dsl.Rec[[X] =>> dsl.<+>(One, (dsl.<*>(A, X)) This could be generalized to allow "extension types" which looks a lot more intuitive to me: extension (dsl: Dsl)
opaque type List[A] =
dsl.Rec[[X] =>> dsl.<+>(One, (dsl.<*>(A, X)) This would avoid the boilerplate for tracking equality, but not the prefix-related boilerplate that can only be mitigated with imports. |
Beta Was this translation helpful? Give feedback.
-
Absolutely.
That would be nice to have. However, that would not give us the ability to |
Beta Was this translation helpful? Give feedback.
-
Interestingly type projections in Scala 2 could be used to achieve reasonable ergonomics in such cases. Here is an example of something you could write: def test[D <: Dsl, A](xs: D#STAR[A, Lists.List[D, A]])(using DslImpl[D]): Lists.List[D, A] =
dsl._2(xs) You lose the neat infix type syntax, but at least you don't have to introduce extremely verbose module hierarchies and the corresponding argument lists, unlike in your motivating example code. And here is the valid Scala 2 code demonstrating how to achieve it: trait Dsl {
type STAR[A, B] // analogous to (A, B)
type PLUS[A, B] // analogous to Either[A, B]
type One // analogous to Unit
type Rec[F[_]] // type-level fixed point
}
trait DslImpl[D <: Dsl] {
def _2[A, B]: (D#STAR[A, B]) => B
def snd[A, B, C](f: B => C): (D#STAR[A, B]) => (D#STAR[A, C])
// ... more methods
}
class Example extends Dsl {
type STAR[A, B] = (A, B)
type PLUS[A, B] = Either[A, B]
type One = Unit
class Rec[F[_]](unrec: F[Rec[F]])
}
object ExampleImpl extends DslImpl[Example] {
def _2[A, B] = _._2
def snd[A, B, C](f: B => C): (Example#STAR[A, B]) => (Example#STAR[A, C]) =
x => (x._1, f(x._2))
}
object Lists {
type List[D <: Dsl, A] =
D#Rec[({type F[X] = D#PLUS[D#One, D#STAR[A,X]]})#F] // Rec, One, <+>, <*> are dependent on the Dsl instance D
// ... methods to work with Lists
def test[D <: Dsl](implicit dsl: DslImpl[D]): (D#STAR[Int, String]) => (D#STAR[Int, Boolean]) =
dsl.snd(_.length > 1)
}
object Main {
def test[D <: Dsl, A](xs: D#STAR[A, Lists.List[D, A]])(implicit dsl: DslImpl[D]): Lists.List[D, A] =
dsl._2(xs)
} Unfortunately type projections were summarily removed from Scala 3 because Scala 2 was wrong about them in a corner case no one actually cares about (related to lower bounds). The thing is we've know how to make type projection sound for a while now: lampepfl/dotty-feature-requests#14 (comment) |
Beta Was this translation helpful? Give feedback.
-
This post is partly a scream for help (as I am drowning in boilerplate) and partly a language feature proposal that would fix the situation.
The basic problem
... is that to define a type that mentions a type member of a
Context
module, one has to place such definition within another class type. Givena type definition like
has to be placed inside a scope where
Context
is available, such asIt is then hard to convince the compiler that, given
the types
c.Foo
andm.c.Foo
are the same type:The consequences for the ergonomics of module-style modularity are huge, as will be demonstrated below.
Proposed solution
Introduce context-dependent types, i.e. types that depend on some contextual value. I will be using the following hypothetical syntax in the rest of this post:
Such context-dependent types are a missing counterpart to context-dependent methods/functions:
Consequences
TL;DR
The current situation
It takes a bit of a build-up to demonstrate the paralyzing amount of boilerplate one has to write (and read!), so please, bear with me.
DSL
Let's have a little DSL
It is an abstract trait, since we will want to have multiple implementations of it.
We will be building modules and applications on top of this DSL, regardless of concrete implementations of the DSL.
Lists
moduleHere is a module defining a
List
data structure on top of the DSL:Notice the
DSL <: Dsl
type parameter. This will help convince the compiler that giventhe types
dsl.One
andlists.dsl.One
are the same type, etc.NonEmptyLists
moduleHere is another module, defining non-empty lists on top of the DSL and the
Lists
module:Notice there are now two type parameters,
DSL <: Dsl, LISTS <: Lists[DSL]
. This will help convince the compiler that giventhe types
lists.List[A]
andnels.lists.Lists[A]
are the same type, as well asdsl.One
,lists.dsl.One
,nels.dsl.One
,nels.lists.dsl.One
are all the same type.Queues
moduleHere's another module,
Queues
, defined again on top of the DSL and theLists
module.Method using all the modules
Now let's implement a method that needs all the modules:
Wow, that is quite some bureaucracy! There ought to be a better way.
The complete typechecked version of these snippets is at https://scastie.scala-lang.org/G1rGxw83Qz2gl6yrDO97AA
Does anyone have a less boilerplatey solution to express this example?
How it could be
Now I will present how things could go if we had context-dependent types, as defined above.
DSL
The DSL trait remains the same
but we also add context-dependent types and functions to its companion object:
Lists
Here is the key point that leads to great simplification: we didn't need to put the
List
type inside a trait as before. That will save us from having to track the equality of the class instances, which was the main cause of complexity.NonEmptyLists
Queues
Using all the modules
So much cleaner!
Beta Was this translation helpful? Give feedback.
All reactions