Contravariant Monoidal
API Documentation: ContravariantMonoidal
The ContravariantMonoidal
type class is for Contravariant
functors that can define a
product
function and a unit
function.
import cats.Contravariant
trait ContravariantMonoidal[F[_]] extends Contravariant[F] {
def unit: F[Unit]
def product[A, B](fa: F[A], fc: F[B]): F[(A, B)]
def contramap2[A, B, C](fb: F[B], fc: F[C])(f: A => (B, C)): F[A] =
contramap(product(fb, fc))(f)
}
Notice that this allows us to define the contramap2
function, much like
the map2
function and the pure
function on the Applicative
typeclass, but in reverse.
Basically, if you have two contexts F[B]
and F[C]
for types
B
and C
, as well as a way to produce types B
and C
simultaneously
from a type A
, then ContravariantMonoidal
allows you to obtain
a context F[A]
for the type A
.
Examples of ContravariantMonoidal
instances are Eq
and Const
,
but there are also interesting instances for other types.
Predicates Have ContravariantMonoidal
An example application would be the case of predicates. Consider the type,
import cats._
import cats.syntax.all._
case class Predicate[A](run: A => Boolean)
Then, we can exhibit a ContravariantMonoidal
for Predicate
by basing it on the
Monoid
for Boolean
via &&
as,
implicit val contravariantMonoidalPredicate: ContravariantMonoidal[Predicate] =
new ContravariantMonoidal [Predicate] {
def unit: Predicate[Unit] = Predicate[Unit](Function.const(true))
def product[A, B](fa: Predicate[A], fb: Predicate[B]): Predicate[(A, B)] =
Predicate(x => fa.run(x._1) && fb.run(x._2))
def contramap[A, B](fa: Predicate[A])(f: B => A): Predicate[B] =
Predicate(x => fa.run(f(x)))
}
We could have also used false
and ||
, but the "and" version
tends to be a little more convenient for this application.
Just like for Contravariant
, we can contramap
to
pull Predicates
back along functions.
case class Money(value: Long)
def isEven: Predicate[Long] = Predicate(_ % 2 == 0)
def isEvenMoney: Predicate[Money] = isEven.contramap(_.value)
isEvenMoney.run(Money(55))
// res1: Boolean = false
We can also lift functions contravariantly into the context instead of contramapping repeatedly.
def times2Predicate: Predicate[Long] => Predicate[Long] =
ContravariantMonoidal[Predicate].liftContravariant((x: Long) => 2*x)
def liftMoney: Predicate[Long] => Predicate[Money] =
ContravariantMonoidal[Predicate].liftContravariant(_.value)
def trivial = times2Predicate(isEven)
trivial.run(2)
// res2: Boolean = true
trivial.run(5)
// res3: Boolean = true
More interestingly, we can combine multiple predicates using
a contramapN
.
case class Transaction(value: Money, payee: String)
def isEvan: Predicate[String] = Predicate(_ == "Evan")
def isGreaterThan50Dollars: Predicate[Money] = liftMoney(Predicate(_ > 50))
def isEvenPaymentToEvanOfMoreThan50 =
(isEvenMoney, isGreaterThan50Dollars, isEvan).contramapN(
(trans: Transaction) => (trans.value, trans.value, trans.payee))
isEvenPaymentToEvanOfMoreThan50.run(Transaction(Money(56), "Evan"))
// res4: Boolean = true