Nice! Most of these changes look excellent to me!
The rules don’t mention anything about subtyping and variance though and I think it’s very, very important to clarify how those factors affect implicit resolution. How does subtyping and variance in return values affect resolution? How does subtyping and variance in implicit parameters affect resolution? Same for “declaration scope”, by which I mean if one were to use the Scala2 inheritance trick I show below, how (if at all) would that affect implicit resolution in Scala 3?
My other feedback is about rule 7. Off the top of my head there are three reasons to use the prioritisation-of-implicits trick:
1. You’re providing generic implicits for multiple subtypes of the same supertype.
trait Functor[F[_]]
trait Monad[F[_]] extends Functor[F]
trait LowPri {
implicit def functor[F[_]]: Functor[F[_]] = ...
}
trait HighPri extends LowPri {
implicit def monad[F[_]]: Monad[F[_]] = ...
}
The goal is that monad should be chosen over functor.
Rule 7 supports this case iff the Monad is considered more specific than Functor due to subtyping.
2. You’re providing specific implicits for HK types parameterised by multiple subtypes of the same supertype.
case class SuperQuickLossyEq[-A](areEq: (A, A) => Boolean)
object SuperQuickLossyEq {
trait LowPri {
implicit val anyref: SuperQuickLossyEq[AnyRef] = apply(_ eq _)
}
trait HighPri extends LowPri {
// silly but just an example
implicit val string: SuperQuickLossyEq[String] = apply(_.length == _.length)
}
}
The goal is that for strings, SuperQuickLossyEq[String]
should be chosen. Scala 2 would go for SuperQuickLossyEq[AnyRef]
because of the contravariance.
Rule 7 supports this case iff the String implicit is considered more specific than the AnyRef one due to subtyping and ignoring typeclass variance.
3. You want to mix specialised implicits with generic ones.
trait MapReduce
trait Parallel
trait LowPri {
implicit def sequential: MapReduce = ...
}
trait HighPri extends LowPri {
implicit def parallel(implicit p: Parallel): MapReduce = ...
}
The goal is that if parallelism is possible, use it, otherwise fallback to a sequential implementation.
Rule 7 actually does the opposite here! According to the rules, A (sequential
) is considered more specific because it has no implicit params where as B (parellel
) does. B should be more specific right? There’s more specificity in the solution / solved-equation. (A) would work with and without a given Parallel; B wouldn’t. You can go from B to A, but not A to B (without adding a P). That’s more specific right? So maybe it’s a typo?
Finally, as to the idea of modelling priority as types and then requiring users import those as well, I can’t see myself ever using that in my libraries. The reason is that all the cases I can think of where I’ve used the prioritisation trick, it’s been to communicate to Scala rules that are actually unambiguous in whatever domain I’m working in. There have been a few rare times where maybe my code, definitely in library or two I’ve used, has a bunch of built in defaults and then there’s a special object that users should import if they want to change the defaults. And actually come to think of it, those scenarios fall into my example.3 above.