Explore “herding cats”: Functor
Functor extends Invariant.
trait Functor[F[_]] extends Invariant[F]
That’s a little bit tricky once invariant functor is typically explained as mix of usual (covariant) and contravariant functors. Those functors are something like:
trait CovariantFunctor[A]:
def map[B](f: A => B): CovariantFunctor[B]
trait ContravariantFunctor[A]:
def contramap[B](f: B => A): ContravariantFunctor[B]
With HKT precise definitions are going to be:
trait CovariantFunctor[F[_]]:
def map[A, B](fa: F[A])(f: A => B): F[B]
trait ContravariantFunctor[F[_]]:
def contramap[A, B](fa: F[A])(f: B => A): F[B]
Let’s return back to the notion of Invariant. Cats Invariant doc and Softwaremill Invarian note provide nice examples. Nevertheless, I want to attempt to bring something extremely down-to-earth.
Let we have some thin wrapper:
trait EqWrapper[T]:
def eqv(valueToCompare: T): Boolean
def get: T
String implementation:
class StringEqWrapper(private val value: String) extends EqWrapper[String] {
def eqv(valueToCompare: String): Boolean = valueToCompare == value
def get: String = value
}
We can assume that in real world we define some non-trivial behaviour. It would be nice to have opportunity to derive new instances from old one using old ones as “back end”.
Let’s attempt to do it with usual map
:
class StringEqWrapper(private val value: String) extends EqWrapper[String] { self =>
def eqv(valueToCompare: String): Boolean = valueToCompare == value
def get: String = value
def map[T](f: String => T): EqWrapper[T] = new EqWrapper[T] {
override def eqv(valueToCompare: T): Boolean = self.eqv("dummy") // I need T => String here to convert value to familiar strings
override def get: T = f(self.get) //map of basic covariant functor works well
}
}
At that point we see that String => T
helped to implement get
. We just apply function to underlying string value and return result.
But we can’t compare T
with String
.
Contravariant approach leads to opposite result.
class StringEqWrapper2(private val value: String) extends EqWrapper[String] { self =>
def eqv(valueToCompare: String): Boolean = valueToCompare == value
def get: String = value
def map[T](f: T => String): EqWrapper[T] = new EqWrapper[T] {
override def eqv(valueToCompare: T): Boolean = self.eqv(f(valueToCompare)) // I know how to convert that T value to well-known String
override def get: T = null.asInstanceOf[T] //I'm in trouble, I have String state but no idea how to return T value
}
}
We can derive implement eqv(valueToCompare: T)
to compare T
with internal String
state.
But get
require something to convert internal String
state to T
.
class StringEqWrapper(private val value: String) extends EqWrapper[String] { self =>
def eqv(valueToCompare: String): Boolean = valueToCompare == value
def get: String = value
def imap[T](f: String => T, g: T => String): EqWrapper[T] = new EqWrapper[T] {
override def eqv(value: T): Boolean = self.eqv(g(value))
override def get: T = f(self.get)
}
}
Now we can derive new instance with imap
:
val stringEqWrapper = new StringEqWrapper3("42")
val intEqWrapper: EqWrapper[Int] = stringEqWrapper.imap(_.toInt, _.toString)
intEqWrapper eqv 42 //true
Typelevel Functor docs provides good description of API.
Explicitly I can denote that it is a right time to pay attention to type lambdas, there is also nice rockthejvm post about it.
import cats.Functor
val listFuntor: Functor[List] = Functor[List]
listFuntor.as(List(1, 2, 3), "a") //List(a, a, a)
val listOfOptionFunctor: Functor[[α] =>> List[Option[α]]] = listFuntor.compose[Option] //Functor[λ[α => F[G[α]]]] in Scala2
listOfOptionFunctor.map(List(Some(1), None))("N" + _) //val res0: List[Option[String]] = List(Some(N1), None)