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 Set
s 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
Aux
pattern or heavy type parameterization, -
and thus effectively deters from more granular modularization.
For example,
PuroLib
above 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
module
depend 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 val
s; - the exact syntax (
module
keyword) 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.