I have a recurrent problem when modelling typeclass hierarchies. So let’s start with a simple example:
trait Monoid[T] {
def apply(lhs: T, rhs: T): T
}
trait Semigroup[T] extends Monoid[T] {
def zero: T
}
Now, the underlying logic that we want to expose is that monoids can be extended to a semigroup. Or to put it another way, if you have a semigroup, you can derive a monoid. Now, in interesting cases, we end up with branching patterns of inheritance – a latice of typeclasses. And because Semigroup extends Monoid, you end up with multiple, clashing instances, and all the mess that this gives us.
trait Monoid[T] { ... }
trait Semigroup[T] given Monoid[T] { ... }
This encoding avoids the inheritance. It means we can only make a semigroup when there is a corresponding monoid. But now when we use it, we need:
def someFunc[T] given (M: Monoid[T], S: Semigroup[T]) = ???
The list of implied instances rapidly explodes. However, given that we have a semigroup, we know there must be a monoid, because semigroup is defined as requiring a monoid. So we can recover this knowledge:
trait Monoid[T] { ... }
trait Semigroup[T] given (val monoid: Monoid[T]) { ... }
implied SemigroupsRequireMonoids [T]
given Semigroup[T] for Monoid[T] = the[Semigroup[T]].monoid
We can now write:
def foo[T] given Semigroup[T] = {
the[Semigroup[T]] // from the given argument
the[Monoid[T]] // via SemigroupsRequireMonoids
}
Now, I freely admit that this is not beautiful. The typeclass lattice is being encoded here by a given val
on the trait and is being unpacked by an implied
instance that extracts it. However, it does avoid the multiple instances problem and the needing to pass everything in explicitly problem.
So I guess my question is if we can make language improvements for scala 3 that get rid of some of this pain. For example, a way of marking a given on a trait as “exporting” that implied value back out again. For example:
trait Semigroup[T] given (implied monoid: Monoid[T]) { ... }
so here, by marking the given monoid as implied, that makes it available again but without needing to expose a val or write the implicit extractor.