A note of appreciation of tracked parameters
First off, I welcome the introduction of tracked parameters, as described in Modularity Improvements.
Although the SetFunctor example given there might not be the best demonstration of their value,
Changing a single line in the example from
class SetFunctor(tracked val ord: Ordering):
to
class SetFunctor[Ord <: Ordering](val ord: Ord):
makes the example work without tracked parameters.
in my experiment I was able to obtain bigger simplifications:
Before
abstract class ScalettoStreams {
type Dsl <: Scaletto
type PuroLib <: libretto.puro.PuroLib[Dsl]
type ScalettoLib <: libretto.scaletto.ScalettoLib[Dsl, PuroLib]
type PuroStreams <: libretto.stream.puro.PuroStreams[Dsl, PuroLib]
val dsl: Dsl
val puroLib: PuroLib & libretto.puro.PuroLib[dsl.type]
val scalettoLib: ScalettoLib & libretto.scaletto.ScalettoLib[dsl.type, puroLib.type]
val underlying: PuroStreams & libretto.stream.puro.PuroStreams[dsl.type, puroLib.type]
// ...
}
new ScalettoStreams {
override type Dsl = scaletto.type
override type PuroLib = lib.type
override type ScalettoLib = sLib.type
override type PuroStreams = pStreams.type
override val dsl = scaletto
override val puroLib = lib
override val scalettoLib = sLib
override val underlying = pStreams
}
After
class ScalettoStreams(
tracked val dsl: Scaletto,
tracked val puroLib: PuroLib[dsl.type],
tracked val scalettoLib: ScalettoLib[dsl.type, puroLib.type],
tracked val underlying: PuroStreams.Of[dsl.type, puroLib.type],
) {
// ...
}
new ScalettoStreams(scaletto, lib, sLib, pStreams)
Object identity as an impediment
Working with the SetFunctor example
trait Ordering:
type T
def compare(t1:T, t2: T): Int
class SetFunctor(tracked val ord: Ordering):
// note that I changed Set to be opaque
opaque type Set = List[ord.T]
def empty: Set = Nil
// ...
object intOrdering extends Ordering:
type T = Int
def compare(t1: T, t2: T): Int = t1 - t2
val IntSet = new SetFunctor(intOrdering)
the type IntSet.Set is (only formally) dependent on object identity of IntSet, which means that, given
val IntSet1 = new SetFunctor(intOrdering)
val IntSet2 = new SetFunctor(intOrdering)
the types IntSet1.Set and IntSet2.Set are formally distinct, even though they are actually the same type List[intOrdering.T].
That’s old news, but …
Why is it an impediment?
It becomes problematic when multiple modules instantiate their own copy of SetFunctor:
class Foo(tracked val ord: Ordering):
val set = SetFunctor(ord)
class Bar(tracked val ord: Ordering):
val set = SetFunctor(ord)
Now, even when Foo and Bar are instantiated with the same Ordering instance, their respective Sets are not interoperable:
val foo = Foo(intOrdering)
val bar = Bar(intOrdering)
val xs: foo.set.Set = bar.set.empty // ERROR
[error] Found: bar.set.Set
[error] Required: foo.set².Set
[error]
[error] where: set is a value in class Bar
[error] set² is a value in class Foo
[error] val xs: foo.set.Set = bar.set.empty
[error] ^^^^^^^^^^^^^
Existing solution snowballs quickly
One might argue that if interoperability of Foo’s and Bar’s Set is needed, they should take the SetFunctor as a parameter:
object SetFunctor:
type Of[Ord <: Ordering] = SetFunctor { val ord: Ord }
class Foo(
tracked val ord: Ordering,
tracked val set: SetFunctor.Of[ord.type],
)
class Bar(
tracked val ord: Ordering,
tracked val set: SetFunctor.Of[ord.type],
)
and then be instantiated with the same SetFunctor instance:
val intSet = SetFunctor(intOrdering)
val foo = Foo(intOrdering, intSet)
val bar = Bar(intOrdering, intSet)
val xs: foo.set.Set = bar.set.empty // OK
This is indeed the approach I use.
class ScalettoStreams(
tracked val dsl: Scaletto,
tracked val puroLib: PuroLib[dsl.type],
tracked val scalettoLib: ScalettoLib[dsl.type, puroLib.type],
tracked val underlying: PuroStreams.Of[dsl.type, puroLib.type],
)
(The mixture of type-parameterization (PuroLib, ScalettoLib) and Aux pattern (PuroStreams.Of) is unimportant.)
However, it
-
leads to bloated dependent parameter lists,
-
requires the
Auxpattern or heavy type parameterization, -
and thus effectively deters from more granular modularization.
For example,
PuroLibabove has 4000+ LoC, but I hesitate to split it to avoid explosion of these parameter lists (taking like 7 different parameters instead ofPuroLib; and that is for each ofScalettoLib,PuroStreams,ScalettoStreams).
Solution: Modules without object identity
Recall that in
class SetFunctor(tracked val ord: Ordering):
opaque type Set = List[ord.T]
the type Set depends only on the parameter ord and not on the identity of SetFunctor. This is a very common case, at least for me.
For example, my
class PuroLib(val dsl: DSL)
defines many types, e.g.
Maybe
Optionally
Unlimited
Multiple
LList
LList1
Endless
Lease
Lock
...
which depend only on dsl, not on PuroLib’s object identity.
I’d like to propose modules without object identity:
module SetFunctor(ord: Ordering):
opaque type Set = List[ord.T]
where
- all member types (incl. class types (not shown here)) of a
moduledepend only on module’s (type- and value-) parameters; - that is, the compiler infers that
SetFunctor(x).Set =:= SetFunctor(x).Set - all constructor parameters are automatically
tracked vals; - the exact syntax (
modulekeyword) is unimportant at this point.
Application
With the new SetFunctor module, the original version of our previous example would just work
class Foo(tracked val ord: Ordering):
val set = SetFunctor(ord)
class Bar(tracked val ord: Ordering):
val set = SetFunctor(ord)
val foo = Foo(intOrdering)
val bar = Bar(intOrdering)
// this is now OK
val xs: foo.set.Set = bar.set.empty
because the type of both foo.set and bar.set is a (pure) function of the same intOrdering. Let’s denote this type as SetFunctor(intOrdering.type). We have:
foo.set : SetFunctor(intOrdering.type)
bar.set : SetFunctor(intOrdering.type)
// therefore
foo.set.Set =:= SetFunctor(intOrdering.type)#Set
bar.set.Set =:= SetFunctor(intOrdering.type)#Set
// therefore
foo.set.Set =:= bar.set.Set
In closing
I believe (and tried to demonstrate) that the proposed feature greatly improves the experience of writing modular code in Scala, thereby opening doors to things that would previously be prohibitively cluttered.
I do realize that my proposal is far from being a specification, but I do hope it is sufficiently clear for people to contemplate and scrutinize.
I also realize that it would require non-trivial changes to the type-checker (treating module names as uninterpreted functions).
Anyway, encouraged by the recent (still experimental) improvements to modularity, I am hoping this effort will continue, including in a direction that addresses the impediment outlined in this post.