Proposal: Restricted Type Projection Sugar

Problem: Parameter Explosion
The removal of type projection (T#A) in Scala 3 prevents us from accessing the arguments of a higher-kinded type parameter. This forces us to “lift” every internal type variable into the class signature.

This is not just combinatorially verbose; it breaks encapsulation. We are forced to declare “useless” parameters—implementation details of the child components—solely to reconstruct the return type in the extends clause.

Example
Consider a Pipe defined by an Input Schema and an Output Schema.

  • Schema has 2 parameters: Row type and Validator type.
  • Merging two Pipes requires merging their schemas (Union of Rows, Intersection of Validators).
trait Schema[+Row, -Val]
trait Pipe[I <: Schema[?,?], O <: Schema[?,?]]

Current Scala (12 Parameters)
To implement MergePipes, we cannot simply take two Pipes. We must extract R1, V1 etc., because the MergeLogic in the extends clause requires them explicitly.

// The Type Logic (Operator)
// We are forced to define this on the raw components
trait MergeLogic[R1, V1, R2, V2] extends Schema[R1 | R2, V1 & V2]

case class MergePipes[
  // Left Pipe Internals
  IR1, IV1, I1 <: Schema[IR1, IV1],
  OR1, OV1, O1 <: Schema[OR1, OV1],
  // Right Pipe Internals
  IR2, IV2, I2 <: Schema[IR2, IV2],
  OR2, OV2, O2 <: Schema[OR2, OV2]
](
  left: Pipe[I1, O1], 
  right: Pipe[I2, O2]
) extends Pipe[
  // Visual Noise: We must manually thread the useless params here
  MergeLogic[IR1, IV1, IR2, IV2], 
  MergeLogic[OR1, OV1, OR2, OV2]
]

Proposed Syntax (2 Parameters)
We define MergeLogic to accept the Schema containers directly, using projection to access Row and Val internally. The MergePipes class now only takes the two pipes.

// The Type Logic (Refactored)
// Now accepts the containers and projects internally
trait MergeLogic[S1 <: Schema[?,?], S2 <: Schema[?,?]] 
  extends Schema[S1#Row | S2#Row, S1#Val & S2#Val]

type PTop = Pipe[?, ?]

case class MergePipes[P1 <: PTop, P2 <: PTop](left: P1, right: P2) 
  extends Pipe[
    // Clean: We extract the schemas from the pipes via projection
    MergeLogic[P1#I, P2#I], 
    MergeLogic[P1#O, P2#O]
  ]

The Proposal
Allow C#T only if C has an upper bound that defines the type parameter T.

This is not general type projection. It is only (equivalent to) syntactic sugar for the explicit parameter version.

1 Like

It is clearly not just syntactic sugar if it makes the number of type parameters dependent on some kind of “overloading” at the type level. :wink: I’m afraid what you’re aiming for is much more involved than your proposal suggests.

I believe there is a misunderstanding of the proposal. There is no “overloading” or dynamic arity resolution involved. The transformation happens strictly at the definition site, statically, just like for context bounds.

When we write def f[T: Ordering], the compiler desugars this into a signature with an additional implicit parameter (using Ordering[T]). The “user-facing” arity differs from the “internal” arity. That is standard syntactic sugar.

My proposal works the same way:

  1. Source: class Pair[C <: Code[...]]
  2. Desugars to: class Pair[P, N, C <: Code[P, N, ...]]

The underlying class always has the exploded parameter list. The sugar merely allows the user to declare it using the high-level shape, with the compiler filling in the fresh type variables for the bounds.

1 Like

Can this case be solved with the tracked modifier? Modularity Improvements

2 Likes

I don’t think so, unfortunately, since it doesn’t help with type parameters, and member types behave rather differently, particularly in terms of variance.

@Stephane Most likely your case can be emulated with General type projections through match types though I think it would be a good idea to make general type projection syntax type check as if such a match type was used.