Proposing further improvement to modularity: modules without object identity

Reviving this thread with a another solution proposal. The previous discussion got a bit sidetracked, so it’s worth noting that the original problem remains unaddressed.

For reference, here's a version that makes the snowballing more apparent.
//> using scala 3.7.0
//> using option -language:experimental.modularity

trait Ordering:
  type T
  def compare(t1:T, t2: T): Int

class SetFunctor(tracked val ord: Ordering):
  opaque type Set = List[ord.T]

  def empty: Set = Nil

object SetFunctor:
  type Of[Ord <: Ordering] =
    SetFunctor { val ord: Ord }

class Foo(
  tracked val ord: Ordering,
  tracked val set: SetFunctor.Of[ord.type],
)

object Foo:
  type Of[Ord <: Ordering, Set <: SetFunctor.Of[Ord]] =
    Foo { val ord: Ord; val set: Set }

class Bar(
  tracked val ord: Ordering,
  tracked val set: SetFunctor.Of[ord.type],
  tracked val foo: Foo.Of[ord.type, set.type],
):
  summon[set.Set =:= foo.set.Set]

object Bar:
  type Of[Ord <: Ordering, Set <: SetFunctor.Of[Ord], F <: Foo.Of[Ord, Set]] =
    Bar { val ord: Ord; val set: Set; val foo: F }

class Baz(
  tracked val ord: Ordering,
  tracked val set: SetFunctor.Of[ord.type],
  tracked val foo: Foo.Of[ord.type, set.type],
  tracked val bar: Bar.Of[ord.type, set.type, foo.type],
):
  summon[set.Set =:= foo.set.Set]
  summon[set.Set =:= bar.set.Set]
  summon[set.Set =:= bar.foo.set.Set]

object Baz:
  type Of[Ord <: Ordering, Set <: SetFunctor.Of[Ord], F <: Foo.Of[Ord, Set], B <: Bar.Of[Ord, Set, F]] =
    Baz { val ord: Ord; val set: Set; val foo: F; val bar: B }

object intOrdering extends Ordering:
  type T = Int
  def compare(t1: T, t2: T): Int = t1 - t2

val IntSet = SetFunctor(intOrdering)
val foo = Foo(intOrdering, IntSet)
val bar = Bar(intOrdering, IntSet, foo)
val baz = Baz(intOrdering, IntSet, foo, bar)

summon[IntSet.Set =:= foo.set.Set]
summon[IntSet.Set =:= bar.set.Set]
summon[IntSet.Set =:= bar.foo.set.Set]
summon[IntSet.Set =:= baz.set.Set]
summon[IntSet.Set =:= baz.foo.set.Set]
summon[IntSet.Set =:= baz.bar.set.Set]
summon[IntSet.Set =:= baz.bar.foo.set.Set]

The above code compiles, but notice the accretion of type parameters, instantiated to singleton types of previous value parameters, most pronounced in the definition of Baz. A module depending on n other modules has up to n type parameters (here seen in the Foo.Of[...] auxiliary types) and instantiates up to O(n2) type parameters of its dependencies to singleton types of its previous dependencies.

The task is to simplify this pattern to avoid the accretion of dependent type parameters.

Prerequisite

My new proposal depends on another old proposal, let’s call it value-dependent type aliases:

type Set(ord: Ordering) = List[ord.T]

(The linked proposal calls them context-dependent types, because the initial motivation there was to use them with implicit (contextual) values, and then generalized by @smarter in the first comment. I then never got to initiating a pre-SIP discussion on it.)

Solution

Introduce static type members, which do not depend on object identity of its owner.

class SetFunctor(tracked val ord: Ordering):
  static opaque type Set = List[ord.T]

The implementation would then move the definition of a static type member to the companion object and put a (non-opaque) type alias in its original position. The rewrite of the above would be

class SetFunctor(tracked val ord: Ordering):
  type Set = SetFunctor.Set(ord) // non-opaque here

object SetFunctor:
 opaque type Set(ord: Ordering) = List[ord.T] // we keep the opaqueness here

Notice that we needed a value-dependent type alias to be able to extract Set out of its owner class.

(Extraction would not be possible if the static type member depended on the object identity of the owner class. In such case, compile-time error would be reported.)

In general, any relocated opaque static type member would be parameterized by the full list of type and value parameters of the owner class:

class MyFunctor[A](val b: B):
  opaque type Collection[T] = <...>

would rewrite to

class MyFunctor[A](val b: B):
  type Collection[T] = MyFunctor.Collection[A](b)[T]

object MyFunctor:
  opaque type Collection <: [A] =>> (b: B) =>> [T] =>> Any = 
    [A] =>> (b: B) =>> [T] =>> <...>

Now, it would be nice if, in the IDE and error messages, the compiler referred to the type Collection[T] as MyFunctor[A](b).Collection[T] instead of MyFunctor.Collection[A](b)[T].

Why is it a solution?

As was noted by @alvae, the problem does not arise for non-opaque type aliases inside modules. By the rewrite, we managed to make the type member non-opaque inside the module (but still keeping it opaque in the companion).


Thoughts? Could it fly? Would you support the value-dependent type aliases proposal?