In the recent discussion about scala 3 migration, the topic of dropped general type projections was brought up.
I understand the general reason for unsoundness, but we have a working and generic enough alternative through match types.
trait Request {
type Result
}
object Request {
type Aux[T] = Request { type Result = T }
type Result[T <: Request] = T match {
case Aux[s] => s
}
}
def foo[T <: Request]: Request.Result[T]
I’m wondering if we could make this pattern more streamlined? Could we use the same logic behind # syntax for type projection? Hence support it again for any type?
With path dependent types, you can do the following:
class Request {
type Result
}
def foo[T <: Request](t: T): t.Result = ???
To me this makes more sense, as you usually want to interact with values at some point, if only to drive type inference
And if you don’t need a value at runtime, then you could use erased (once it’s official): Erased Definitions
My question is: Are there cases where this is not enough/worse ?
In workflows4s I use types as “containers” to reduce the number of type parameters.
trait WorkflowContext {
type Event
type State
}
trait WIO[-In, +Err, +Out <: WCState[Ctx], Ctx <: WorkflowContext]
Yet I don’t ever need the value of that context type. Initially I tried to make it all path-dependent but it wasn’t ergonomic (I don’t remember reasons though).
The bottom line is: while path-dependent types are a solution, I believe match types extractors are more universal, it’s just syntax that’s missing.
Note that the technique with match types is not nearly as powerful as Scala 2’s general type projections. Otherwise it would be just as unsound, obviously.
The main difference is that it refuses to reduce in any way until one of two things happen:
you get a stable prefix, or
the prefix gets precise enough that the type member resolves to a type alias whose right-hand-side does not depend on the prefix.
Concretely, that means X#T <: A will not be true for something where X has { type T <: A }.
It also means the match types technique is not always capable of representing valid Scala 3 type projections. In particular, referring to Java non-static inner classes. I don’t think we could “desugar” type projections as the match types trick.
I don’t think we could “desugar” type projections as the match types trick.
I don’t know if we think about the same, but my original idea was: if we already support 20% of types (concrete classes) with X#T syntax, now we could cover another 40% plugging in match types under the hood.
So still not a full solution but imho a significant improvement.
Type projections are a huge code smell. They were originally intended to model Java’s inner classes only. Then some crucial test got dropped (by accident?) in the 2.x compiler and type projections became super powerful and unsound. But every usage I have seen looked bad. And no other language I know has something resembling it, except for Java’s inner class references.
So to everyone: Please try to find solutions without type projections. I bet your code will become clearer and more reliable.
What would you recommend for the use case presented here of packaging multiple type parameters in a bundle ?
I assume something like this:
trait WorkflowContext {
type Event
type State
}
trait WIO[-In, +Err, +Out <: WCState[Ctx]](using val ctx: WorkflowContext):
val exampleEvent: ctx.Event
val exampleState: ctx.State
(With potentially erased as discussed above)
This solution seems to me equivalent to the Ctx#Event solution.
Therefore, I think we should find out if and why it wasn’t ergonomic:
This would allow us to know if this kind of type selection is worthwhile or what we can improve about givens/path-dependent types
I wasnt too clear with my example, sorry, but WCState[Ctx] is actually my match type extractor so it would have to be something like this
trait WIO[-In, +Err, +Out <: ctx.State](using val ctx: WorkflowContext)
And this doesn’t compile.
Also, the general problem with this approach, IIRC, was that it was really hard to operate on abstract Context. Sometimes you have the right type at hand (hence the value) but sometimes you want to say “for any value/context”.