allidoiswin
allidoiswin

Reputation: 2589

Scala: deferring a trait method to an implicit class in parent trait's object

Specifically, I'm trying to extend my Functor typeclass with Applicative.

trait Functor[F[_]] {
  def fmap[A, B](r: F[A], f: A => B): F[B]
}

object Functor {
  implicit class FunctorOps[A, F[_]: Functor](xs: F[A]) {
    def fmap[B](f: A => B): F[B] = implicitly[Functor[F]].fmap(xs, f)
  }

  implicit def SeqFunctor: Functor[Seq] = new Functor[Seq] {
    def fmap[A, B](r: Seq[A], f: A => B) = r map f
  }
}

trait Applicative[F[_]] extends Functor[F] {
// What I want to do, but this *does not* work.
  def fmap[A, B](r: F[A], f: A => B): F[B] = Functor.FunctorOps[A, F](r).fmap(f)

  def pure[A](x: A): F[A]
  def fapply[A, B](r: F[A], f: F[A => B]): F[B]
}

object Applicative {
  implicit class ApplicativeOps[A, F[_]](a: F[A])(implicit F: Applicative[F]) {
    def fapply[B](f: F[A => B]): F[B] = F.fapply(a, f)
  }

  implicit def SeqApplicative: Applicative[Seq] = new Applicative[Seq] {
    def pure[A](x: A) = Seq(x)
    def fapply[A, B](xs: Seq[A], fs: Seq[A => B]): Seq[B] = xs.flatMap(x => fs.map(_(x)))
  }
}

The gist of it is I have to implement fmap for all Applicatives, but it should really be the same method as defined in my FunctorOps class. How do I do this in the cleanest way possible?

Upvotes: 0

Views: 108

Answers (1)

HTNW
HTNW

Reputation: 29193

You got the Applicative[F] <: Functor[F] part right, but you should really think about what that means. It means that an instance of Applicative[F] also provides the methods for Functor[F]. That is, you can't have both implicit val listFunctor: Functor[List]; implicit val listApplicative: Applicative[List], because then the compiler is confused when you ask for implicit param: Functor[List]. You should only have the latter. What you're trying to do is then nonsensical, because you're defining the Functor instance for F in terms of itself (because the Applicative[F] should be the Functor[F]), and you end up with two Functor[Seq]s regardless.

What you can do is implement fmap in terms of pure and fapply:

trait Applicative[F[_]] extends Functor[F] {
  override def fmap[A, B](r: F[A], f: A => B): F[B] = fapply(r, pure(f))

  def pure[A](x: A): F[A]
  def fapply[A, B](r: F[A], f: F[A => B]): F[B]
}

Then remove the Functor[Seq] instance and keep your Applicative[Seq] the way it is (or override fmap if you want). You have a different problem now, being that implicit search gets a bit turned around, as the Functor[Seq] instance is actually in object Applicative, but fixing implicit resolution is less onerous than enforcing the consistency of separate Functor and Applicative instances. In this case, where Seq is a type whose companion object you cannot control, cats, at least, does something like

package instances {
  trait SeqInstances {
    implicit val seqFunctor: Functor[Seq] = ???
  }
  package object seq extends SeqInstances
  package object all extends SeqInstances
                        with AAAInstances
                        with BBBInstances
                        with ...
}

FYI: I suggest currying your typeclass methods

def fmap[A, B](r: F[A])(f: A => B): F[B]
def fapply[A, B](r: F[A])(f: F[A => B]): F[B]

and it may be nice to have a flip fapply in ApplicativeOps

def ylppaf[I, B](f: F[I])(implicit ev: A =:= (I => B))
  : F[B] = F.fapply(a.fmap(ev))(f)

Upvotes: 1

Related Questions