Bifoldable
API Documentation: Bifoldable
Bifoldable[F[_,_]] instances identify data structures with two independent Foldable that fold to the same summary value.
As a reminder Foldable is implemented in terms of foldLeft and foldRight; similarly Bifoldable is implemented in terms of:
//eagerly performs a left-associative bi-fold over `fab`
def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C
//lazily performs a right-associative bi-fold over `fab`
def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C]
and by implementing those 2 methods you also get:
def bifold[A, B](fab: F[A, B])(implicit A: Monoid[A], B: Monoid[B]): (A, B)
def bifoldMap[A, B, C](fab: F[A, B])(f: A => C, g: B => C)(implicit C: Monoid[C]): C
A lawful instance must have bifoldLeft\Right consistent with bifoldMap; left\right bi-folds based on associative
functions should output similar results.
Either and Validated as Bifoldable
Assume multiple input paths and system requests that end up as Either[Exception, *] or Validated[Exception, *].
The requirement is to track the amount of errors and store the content of valid requests.
First add the implicits:
import cats._
import cats.data._
import cats.syntax.all._
then let's define a summary class capable of storing this info:
case class Report(entries: Chain[String], errors: Int) {
def withEntries(entry: String): Report =
this.copy(entries = entries :+ entry)
def withError: Report =
this.copy(errors = errors + 1)
}
Bifoldable is useful to get a summary value from F[_,_] data types:
def update[F[_, _]: Bifoldable](current: Report)(result: F[Exception, String]): Report =
result.bifoldLeft(current)((acc, _) => acc.withError, (acc, user) => acc.withEntries(user))
Here is the system input:
val validated =
List(
Validated.valid[Exception, String]("valid request 1"),
Validated.valid[Exception, String]("valid request 2"),
Validated.invalid[Exception, String](new RuntimeException("Not a valid request"))
)
// validated: List[Validated[Exception, String]] = List(
// Valid(a = "valid request 1"),
// Valid(a = "valid request 2"),
// Invalid(e = java.lang.RuntimeException: Not a valid request)
// )
val attempted =
List(
Either.right[Exception, String]("valid request 1"),
Either.right[Exception, String]("valid request 2"),
Either.left[Exception, String](new RuntimeException("Not a valid request"))
)
// attempted: List[Either[Exception, String]] = List(
// Right(value = "valid request 1"),
// Right(value = "valid request 2"),
// Left(value = java.lang.RuntimeException: Not a valid request)
// )
and bi-fold each value into the accumulator:
val empty = Report(Chain.empty, 0)
// empty: Report = Report(entries = Chain(), errors = 0)
validated
.foldl(empty)((acc, validation) => update(acc)(validation))
// res0: Report = Report(
// entries = Append(
// leftNE = Singleton(a = "valid request 1"),
// rightNE = Singleton(a = "valid request 2")
// ),
// errors = 1
// )
attempted
.foldl(empty)((acc, attempt) => update(acc)(attempt))
// res1: Report = Report(
// entries = Append(
// leftNE = Singleton(a = "valid request 1"),
// rightNE = Singleton(a = "valid request 2")
// ),
// errors = 1
// )
Tuple as Bifoldable
Assume we have (String, String, Int) and to get our summary we need _1 and _3.
The existing implementations for (*, *), (T0, *, *), (T0, T1, *, *) .. aren't useful.
Let's make a new Bifoldable instance:
implicit def bifoldableForTuple3[A0]: Bifoldable[(*, A0, *)] =
new Bifoldable[(*, A0, *)] {
def bifoldLeft[A, B, C](fa: (A, A0, B), c: C)(f: (C, A) => C, g: (C, B) => C): C =
g(f(c, fa._1), fa._3)
def bifoldRight[A, B, C](fa: (A, A0, B), c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] =
g(fa._3, f(fa._1, c))
}
As we were saying in the beginning a lawful Bifoldable should have bifoldLeft\Right consistent with bifoldMap.
Let's check our instance:
//(name, age, occupation)
val description = ("Niki", 22, "Developer")
// description: (String, Int, String) = ("Niki", 22, "Developer")
val expected =
Bifoldable[(*, Int, *)].bifoldMap(description)(s => s, s => s)
// expected: String = "NikiDeveloper"
val left =
Bifoldable[(*, Int, *)].bifoldLeft(description, Monoid[String].empty)(
(acc, s) => acc |+| s,
(acc, s) => acc |+| s
)
// left: String = "NikiDeveloper"
val right =
Bifoldable[(*, Int, *)].bifoldRight(description, Eval.later(Monoid[String].empty))(
(s, acc) => acc.map(_ |+| s),
(s, acc) => acc.map(_ |+| s)
)
// right: Eval[String] = cats.Eval$$anon$1@780b7b1a
left === expected
// res2: Boolean = true
right.value === expected
// res3: Boolean = true
NOTE: This instance would not be lawful if bifoldRight in particular would use a different ordering.
Going from right to left as opposed to left to right means:
def bifoldRight[A, B, C](fa: (A, Int, B), c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] =
f(fa._1, g(fa._3, c))
and it would also reverse the output making the instance unlawful:
val reversedRight =
bifoldRight(description, Eval.later(Monoid[String].empty))(
(s, acc) => acc.map(_ |+| s),
(s, acc) => acc.map(_ |+| s)
)
// reversedRight: Eval[String] = cats.Eval$$anon$1@ad59da7
reversedRight.value === expected
// res4: Boolean = false
Bifoldable compose
If F[_,_] and G[_,_] have Bifoldable instances then so does F[G[_,_],G[_,_]].
Examples for (Either[*, *], Either[*, *]) and ((*, *), (*, *)):
Bifoldable[(*, *)]
.compose(Bifoldable[(*, *)])
.bifoldLeft((("name1 ", 1000),("name2 ", 2000)), List.empty[String])((acc, name) => acc :+ name, (acc, _) => acc)
// res5: List[String] = List("name1 ", "name2 ")
Bifoldable[(*, *)]
.compose(Bifoldable[Either[*, *]])
.bifold((Left(1), Right(2)))
// res6: (Int, Int) = (1, 2)