Happy Friday! Today we have a great article written by Afsal Thaj on Invariant Functors Unlocked. This will help you understand invariant functors with Scala, we hope you enjoy this read.
'We tend to forget this quite often. An invariant functor or an exponential functor is, given A => B and B => A , it converts type A to typeB in the same context F[_]. We call this xmap .
trait InvariantFunctor[F[_]] {
def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B]
}
Covariant Functor
trait Functor[F[_]] { self =>
def map[A, B](fa: F[A], f: A => B): F[B]
def xmap[A, B](fa: F[A], f: A => B, g: B => A) =
map(fa, f)
}
Contravariant Functor
trait ContraVariantFunctor[F[_]] {
def contramap[A, B](f: B => A): F[B]
def xmap(fa: F[A], f: A => B, g: B => A): F[B] =
contramap(fa, g)
}
Example for Contravariant Functor
// Write Json. In this case the type parameters in the type class
// came at the method parameters level (contra variant position), calling for having
// a `Contramap`
trait EncodeJson[A] { self =>
def toJson(a: A): Json
def contramap(f: B => A): EncodeJson[B] = new EncodeJson[B] {
def toJson(a: B): Json = self.toJson(f(b))
}
} // Implies EncodeJson can have an instance of contravariant functor
`
Usage
case class Something(value: Int)
// We have implicit EncodeJson available for Int.
def toJson(a: Int) = EncodeJson[Int].toJson(a)
def toJson(a: Something): EncodeJson[Something] =
EncodeJson[Int].contramap { _.value }
Example for covariant Functor
trait DecodeJson[A] {
def fromJson(a: Json): A
def map[B](f: A => B): DecodeJson[B] = new DecodeJson[B] {
def fromJson(a: Json): B = f(self.fromJson(a))
}
}
Note
If type parameters are at covariant position, that means the method return contains the type.
If type parameters are at contravariant position, that means the method parameters contain the type.
When is invariant functor?
We may have types at covariant (output) or contravariant (input) position. However, we may sometime deal with both covariance and contravariance in the same type class.
Let’s bring in EncodeJson and DecodeJson into one type class.
EncodeJson and DecodeJson
trait CodecJson[A] { self =>
def fromJson(a: Json): A
def toJson(b: A): Json
def xmap(f: A => B, g: B => A): CodecJson[B] =
new CodecJson[B] {
def fromJson(a: Json): B = f(self.fromJson(a))
def toJson(b: B): Json = toJson(g(b))
}
}
Functor but invariant
So an individual map or contramap to upcast (or downcast) an A to B in the context of F[_] is not possible if F has types both in covariant and contravariant positions. It means, F has to have an invariant functor for it!