I've been playing around with the typeclass pattern in Scala to better understand how it works, as I'm familiar with Scalaz and wanted to figure out how it works "under the hood".
(You can run the following code with https://ammonite.io/ REPL)
import $plugin.$ivy.`org.spire-math::kind-projector:0.9.3`
sealed trait Maybe[+A] // Option
final case class Just[+A](value: A) extends Maybe[A]
final case object Null extends Maybe[Nothing]
sealed trait Direction[+E, +A] // Either
final case class Up[+E, +A](value: A) extends Direction[E, A]
final case class Down[+E, +A](value: E) extends Direction[E, A]
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
object FunctorSyntax {
implicit final class FunctorExtensions[F[_], A](private val self: F[A]) extends AnyVal {
def map[B](f: A => B)(implicit instance: Functor[F]): F[B] = {
instance.map(self)(f)
}
}
}
object FunctorInstances {
implicit val maybeFunctorInstance: Functor[Maybe] = new Functor[Maybe] {
def map[A, B](fa: Maybe[A])(f: A => B): Maybe[B] = fa match {
case Just(a) => Just(f(a))
case n@Null => n
}
}
implicit def directionFunctorInstance[E]: Functor[Direction[E, ?]] = new Functor[Direction[E, ?]] {
def map[A, B](fa: Direction[E, A])(f: A => B): Direction[E, B] = fa match {
case Up(a) => Up(f(a))
case Down(e) => Down(e)
}
}
}
So I've written some analogues to Option
(Maybe
) and Either
(Direction
), the Functor
definition, some instances for Functor
, some some syntax extensions so I can call .map
on valid functors.
The following code works:
import FunctorInstances._
import FunctorSyntax._
val x: Maybe[Int] = Just(5)
println(x.map(_ + 1)) // prints "Just(6)"
as expected. But the following does not:
val y: Direction[String, Int] = Up(5)
println(y.map(_ + 1)) // errors out
throwing the error help.sc:48: value map is not a member of ammonite.$file.help.Direction[String,Int]
Simply put, I do not want this error to happen, and for .map
to work on an arbitrary Direction[E, ?]
.
I'm thinking that Scala isn't able to see that Direction[String, Int]
can be destructured into a F = Direction[String, ?]
and A = String
, preventing the FunctorExtensions
class from wrapping itself around the val y: Direction[String, Int]
. Unfortunately, I do not know how to fix this.
NB: the instance itself is still recoverable with an implicitly
val instance = implicitly[Functor[Direction[String, ?]]]
println(instance.map(y)(_ + 1)) // prints "Up(6)"
You are probably missing the -Ypartial-unification
scalac option if you are using Scala 2.11 or Scala 2.12.
Add scalacOptions += "-Ypartial-unification"
to your build or you could use Scala 2.13 which has it on by default.
See https://github.com/typelevel/cats/blob/v1.6.1/README.md#getting-started