My team has been trying to develop a subset of Scala that only uses typeclasses and no inheritance. Along the way, we have identified some relatively small changes to Scala (like wildcard context bounds) that e.g. Rust has and make life a lot better.
One thing that we have been struggling with is how to make a function that returns an object that implements a typeclass, but without a static guarantee of what that object will be. In Rust, one can return a Box<dyn Typeclass>
and it is possible to call any method defined on Typeclass
on the resulting pointer. I had hoped this would be possible with export
in Scala 3, but that doesn’t quite work as hoped. I found something that gets pretty close to a nice interface that I wanted to share, both to let folks know that it’s possible, but also to ask if there’s anything even better I’m missing.
class BoxImpl[Self, Typeclass[_]](val self: Self)(using inst: Typeclass[Self]) {
inline def apply[U](f: Typeclass[Self] => Self => U): U = f(inst)(self)
inline def apply[U, Arg](f: Typeclass[Self] => Self => Arg => U)(arg:Arg): U = f(inst)(self)(arg)
inline def apply[U, Arg1, Arg2](f: Typeclass[Self] => Self => (Arg1, Arg2) => U)(arg1:Arg1, arg2: Arg2): U = f(inst)(self)(arg1, arg2)
// ... for other arities as well
}
type Box[Typeclass[_]] = BoxImpl[_, Typeclass]
object Box {
def apply[Self, Typeclass[_]](self: Self)(using Typeclass[Self]): Box[Typeclass] = BoxImpl(self)
def unapply[Typeclass[_]](box: Box[Typeclass]): Some[Any] = Some(box.self)
}
with the above, it’s possible to write a method like
def branch(b: Boolean): Box[MyTypeclass] = {
if (b) Box("4") else Box(3) // assuming given instances MyTypeclass for String and Int
}
and then do
val boxed = branch(true)
boxed(_.nullaryMethod) // assuming extension (self: Self) def nullaryMethod: String on MyTypeclass
boxed(_. unaryMethod)(1) // assuming extension (self: Self) def unaryMethod(x: Int): String on MyTypeclass
You can also unbox using
boxed match {
case Box("5") => ...
}
It’d be nice to have the compiler enforce that there exists an implementation of MyTypeclass
for String
in the pattern, but that’s not possible without some changes to type inference in patterns.
It’s nice, but it’d be even better if one could do boxed.nullaryMethod
and boxed.nameOf(1)
. I am aware that one could manually write out extension methods using extension (Box[MyTypeclass])
, but that was something I was specifically trying to avoid doing.