I've been following along with this blog post, trying to understand how to emulate existentially quantified types using path-dependent types in Scala 3: https://dev.to/raquo/existential-crisis-implementing-mapk-in-scala-3-2fo1
And then I made the following example.
First we define monoids:
trait Monoid[A]:
val id: A
def combine(l: A, r: A): A
object StringMonoid extends Monoid[String]:
val id = ""
def combine(l: String, r: String) = l + r
object AdditiveIntMonoid extends Monoid[Int]:
val id = 0
def combine(l: Int, r: Int) = l + r
object MultiplicativeIntMonoid extends Monoid[Int]:
val id = 1
def combine(l: Int, r: Int) = r * r
Now suppose I want to write code that can take a set of monoids that may not all have the same underlying type. For example
def ids(ms: Monoid*) = // This won't compile because Monoid with no argument
for { m <- ms } yield m.id // is not a type
or
def asList(pairs: ((E, Monoid[E]) for any E)*) = // This is also not valid scala
pairs.toList
I can, with some work, achieve the behavior I want by following the pattern in the blog.
First define a type with a path-dependent inner type to emulate forSome:
type any[F[_]] = {
type Member;
type Ops = F[Member]
}
and a couple of poorly named implicit conversions to help me make instances of the relevant types:
given any_algebra[F[_], A]: Conversion[F[A], any[F]#Ops] = _.asInstanceOf[any[F]#Ops]
given any_algebra_with_member[F[_], A]: Conversion[(A, F[A]), (any[F]#Member, any[F]#Ops)] =
_.asInstanceOf[(any[F]#Member, any[F]#Ops)]
And now I can write
def ids(ms: List[any[Monoid]#Ops]) =
for { m <- ms } yield m.id
def all[F[_]](fs: any[F]#Ops*) = fs.toList
val ms = all(StringMonoid, MultiplicativeIntMonoid, AdditiveIntMonoid, StringMonoid)
val units = ids(all(AdditiveIntMonoid, StringMonoid, MultiplicativeIntMonoid))
def many[F[_]](pairs: (any[F]#Member, any[F]#Ops)*) = pairs.toList
val mms = many(
7 -> AdditiveIntMonoid,
3 -> MultiplicativeIntMonoid,
"foo" -> StringMonoid,
"bar" -> StringMonoid
)
and it all works. I cannot write
val ms = all(StringMonoid, "hello", AdditiveIntMonoid, StringMonoid)
because "hello" is not a Monoid, or
val mms = many(
"foo" -> StringMonoid,
3 -> StringMonoid
)
because 3 is not a string.
My question is why this last part works as I want it to. Why can't I write many(3 -> StringMonoid)? In def many[F[_]](pairs: (any[F]#Member, any[F]#Ops)*), what constrains both appearances of any[F] within the tuple type to refer to the same type? Where (in the Scala language specification or anywhere else) should I start reading to be able to understand this from first principles?
It's because you have this implicit conversion
manyaccepts tuples of(any[F]#Member, any[F]#Ops). So when you supply the tuple"foo" -> StringMonoidthe compiler uses that implicit conversion to convert it to a tuple of the requested type. But that conversion only works for tuples that conform to the shape of(A, F[A]). I.e. theAin both members has to be the same type. But when you supply3 -> StringMonoidthe leftAisIntand the rightAisString, so the implicit conversion will not work.The compiler may still try to make it work by inferring
A = Any, butMonoid[String]is not a subtype ofMonoid[Any]because it is invariant. So that won't work either.