Bifunctor
API Documentation: Bifunctor
Bifunctor
takes two type parameters instead of one, and is a functor in both
of these parameters. It defines a function bimap
, which allows for mapping over both
arguments at the same time. Its signature is as follows:
def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D]
Either as a Bifunctor
Probably the most widely used Bifunctor instance is the Either data type.
Say you have a value that is either an error or a ZonedDateTime
instance.
You also want to react to both possibilities - if there was a failure, you want to
convert it to your own DomainError
, and if the result was a success, you want to
convert it to an UNIX timestamp.
import cats._
import cats.syntax.all._
import java.time._
case class DomainError(message: String)
def dateTimeFromUser: Either[Throwable, ZonedDateTime] =
Right(ZonedDateTime.now()) // Example definition
dateTimeFromUser.bimap(
error => DomainError(error.getMessage),
dateTime => dateTime.toEpochSecond
)
// res0: Either[DomainError, Long] = Right(value = 1734758698L)
Bifunctor
also defines a convenience function called leftMap
, which is defined as follows:
def leftMap[A, B, C](fab: F[A, B])(f: A => C): F[C, B] = bimap(fab)(f, identity)
There is no rightMap
however - use map
instead. The reasoning behind this is that in Cats, the instances of
Bifunctor
are also mostly instances of Functor
, as it is the case with Either
.
Tuple2 as a Bifunctor
Another very popular Bifunctor
is that for the Tuple2
data type, or (A, B)
for types A
and B
.
Let's say we have a list of balances and want divide them by the number of months in the lifetime of the account holder. The balances are given in cents. A bit contrived, but we want an average contribution per month to the given account. We want the result in dollars per month. The lifetime is given in the number of years the account has been active.
val records: List[(Int, Int)] = List((450000, 3), (770000, 4), (990000, 2), (2100, 4), (43300, 3))
// records: List[(Int, Int)] = List(
// (450000, 3),
// (770000, 4),
// (990000, 2),
// (2100, 4),
// (43300, 3)
// )
def calculateContributionPerMonth(balance: Int, lifetime: Int) = balance / lifetime
val result: List[Int] =
records.map(
record => record.bimap(
cents => cents / 100,
years => 12 * years
)
).map((calculateContributionPerMonth _).tupled)
// result: List[Int] = List(125, 160, 412, 0, 12)
As you can see, this instance makes it convenient to process two related pieces of data in independent ways, especially when there is no state relationship between the two until processing is complete.
Note that, just as with the bifunctor for Either
, we do not have a rightMap
function since the relevant instances of Bifunctor
induce a Functor
in the second argument, so we just use map
.