I find extension methods in type classes a little bit cumbersome at the moment.
Here’s what I believe is the common way of using them:
trait Functor[F[_]]:
extension [A](lhs: F[A]) def map[B](f: A => B): F[B]
given Functor[Option] with
extension [A](lhs: Option[A]) def map[B](f: A => B) = lhs.map(f)
This suffers from two flaws.
The first one is mostly a matter of opinion: instance declaration sites have to jump through unnecessary hoops. They really don’t care that map
is an extension method, and it makes the code more verbose than necessary. I would personally prefer something that might be a little more unpleasant at the trait
declaration (written once) and a little bit lighter at the instance declaration (written, hopefully, many times).
The second one is probably a bug: it breaks SAM type inference, as pointed out by Nadav Wiener:
trait Semigroup[A]:
extension (lhs: A) def combine(rhs: A): A
given bad: Semigroup[Int] = _ + _
// Wrong number of parameters, expected: 1
I’ve been toying with a different way (spoiler warning: it ends up being even less practical): an abstract method, and a concrete extension method that proxies calls to the abstract one.
For example:
trait Functor[F[_]]:
def fmap[A, B](fa: F[A])(f: A => B): F[B]
extension [A](lhs: F[A]) def map[B](f: A => B): F[B] = fmap(lhs)(f)
given Functor[Option] with
def fmap[A, B](oa: Option[A])(f: A => B) = oa.map(f)
It does make instance declaration more pleasant, but runs into a different problem: you can’t have a regular method and an extension one with the same name. This forces me to find two names for a given method, which is basically a show stopper.
Diego Alonso proposed a different strategy - making the extension method syntax non-compulsory in subclasses.