Search code examples
scalascala-cats

Writing a single implicit class for both a value and a functor of that value


I often find myself having to perform what is pretty much the same operation on both a value and also a functor of that value. I usually achieve this with two implicit classes, like this:

implicit class Apimped(a: A) {
  def doSomething: B = ???
}

implicit class FApimped[F[_]: Functor](fa: F[A]) {
  def doSomething: F[B] = Functor[F].map(fa)(a => a.doSomething)
}

So then I can do this, for example:

a.doSomething //B
Option(a).doSomething //Option[B]

However, it seems a bit unwieldy having to write two implicit classes (often for each value type) to do this. My question is, is there anyway to achieve the above with only a single implicit class? That is, the map operation would be implicit in cases when you call doSomething on a functor of the value. Thanks.


Solution

  • I don't know whether it's in Scalaz/Cats (maybe, cannot guarantee that it's not there), but in principle, it does work. Here is a little demo without any dependencies that demonstrates the principle.

    Assume you have these typeclasses from either Scalaz or Cats:

    import scala.language.higherKinds
    
    trait Functor[F[_]] {
      def map[A, B](a: F[A])(f: A => B): F[B]
    }
    
    type Id[X] = X
    implicit object IdFunctor extends Functor[Id] {
      def map[A, B](a: A)(f: A => B): B = f(a)
    }
    implicit object OptionFunctor extends Functor[Option] {
      def map[A, B](a: Option[A])(f: A => B) = a map f
    }
    

    you can then either write or find in the library a typeclass that works like the following contraption:

    trait EverythingIsAlwaysAFunctor[A, B, F[_]] {
      def apply(a: A): F[B]
      def functor: Functor[F]
    }
    
    object EverythingIsAlwaysAFunctor {
      implicit def functorIsFunctor[A, F[_]](implicit f: Functor[F])
      : EverythingIsAlwaysAFunctor[F[A], A, F] = {
        new EverythingIsAlwaysAFunctor[F[A], A, F] {
          def apply(fa: F[A]): F[A] = fa
          def functor: Functor[F] = f
        }
      }
    
      implicit def idIsAlsoAFunctor[A]
      : EverythingIsAlwaysAFunctor[A, A, Id] = {
        new EverythingIsAlwaysAFunctor[A, A, Id] {
          def apply(a: A): Id[A] = a
          def functor: Functor[Id] = implicitly[Functor[Id]]
        }
      }
    }
    

    This thing does the following:

    1. If the value A is already of shape F[B] for some functor F, then uses this functor
    2. In all other cases, it pretends that A is actually Id[A]

    Now you can write your DoSomething-pimp-my-library-syntax thing with a single doSomething method:

    implicit class DoSomething[A, F[_]](a: A)(
      implicit eiaaf: EverythingIsAlwaysAFunctor[A, Int, F]
    ) {
      def doSomething: F[String] = eiaaf.functor.map(eiaaf(a))("*" * _)
    }
    

    and then it just works in all cases:

    val x = Option(42).doSomething
    val y = 42.doSomething
    
    println(x)
    println(y)
    

    prints:

    Some(******************************************)
    ******************************************