From c18c3f3aa864de0459eb5e9e0d58bdb890c1b71a Mon Sep 17 00:00:00 2001 From: Stephen Lazaro Date: Mon, 8 Oct 2018 17:16:30 -0700 Subject: [PATCH 1/4] Add Simple Tuple2 Bifunctor Example --- docs/src/main/tut/typeclasses/bifunctor.md | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/src/main/tut/typeclasses/bifunctor.md b/docs/src/main/tut/typeclasses/bifunctor.md index 4f3bc24083..b98aa143ff 100644 --- a/docs/src/main/tut/typeclasses/bifunctor.md +++ b/docs/src/main/tut/typeclasses/bifunctor.md @@ -50,3 +50,34 @@ 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. +A bit contrived, but we want an average contribution per month to the given account. +The list of balances is given as a list of numeric strings (except when they aren't), and the account lifetime is given in years. + +```tut:book +val records: List[String] = List("4", "77", "99", "21", "oops") +val lifetimes: List[Int] = List(5, 25, 3, 4, 30) +val withLifetime: List[(String, Int)] = records.zip(lifetimes) + +def decodeInt(s: String): Either[Throwable, Int] = Either.catchNonFatal(s.toInt) + +val result: List[Int] = + withLifetime.map( + _.leftMap(decodeInt) + ).map( + _.bimap({ + case Right(v) => v + case Left(_) => 0 + }, + years => 12 * years + )).map({ + case (balance, lifetime) => balance / lifetime + }) +``` + +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. From 34394ae0ee494338e2d789353a19832031a447e2 Mon Sep 17 00:00:00 2001 From: Stephen Lazaro Date: Mon, 8 Oct 2018 17:18:50 -0700 Subject: [PATCH 2/4] Bit about relation to `Functor` --- docs/src/main/tut/typeclasses/bifunctor.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/main/tut/typeclasses/bifunctor.md b/docs/src/main/tut/typeclasses/bifunctor.md index b98aa143ff..18782d1de4 100644 --- a/docs/src/main/tut/typeclasses/bifunctor.md +++ b/docs/src/main/tut/typeclasses/bifunctor.md @@ -81,3 +81,5 @@ val result: List[Int] = ``` 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`. From bc217dcc38b2628bce8e8dc551ba3cec88ffe085 Mon Sep 17 00:00:00 2001 From: Stephen Lazaro Date: Thu, 18 Oct 2018 14:25:30 -0700 Subject: [PATCH 3/4] Clarify example --- .../main/resources/microsite/data/menu.yml | 3 +++ docs/src/main/tut/typeclasses/bifunctor.md | 25 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/src/main/resources/microsite/data/menu.yml b/docs/src/main/resources/microsite/data/menu.yml index 54a36b3544..9134b256d3 100644 --- a/docs/src/main/resources/microsite/data/menu.yml +++ b/docs/src/main/resources/microsite/data/menu.yml @@ -79,6 +79,9 @@ options: url: typeclasses/invariantmonoidal.html menu_section: variance + - title: Bifunctor + url: typeclasses/bifunctor.html + menu_type: typeclasses - title: Eq url: typeclasses/eq.html diff --git a/docs/src/main/tut/typeclasses/bifunctor.md b/docs/src/main/tut/typeclasses/bifunctor.md index 18782d1de4..807e722501 100644 --- a/docs/src/main/tut/typeclasses/bifunctor.md +++ b/docs/src/main/tut/typeclasses/bifunctor.md @@ -60,24 +60,25 @@ A bit contrived, but we want an average contribution per month to the given acco The list of balances is given as a list of numeric strings (except when they aren't), and the account lifetime is given in years. ```tut:book -val records: List[String] = List("4", "77", "99", "21", "oops") -val lifetimes: List[Int] = List(5, 25, 3, 4, 30) +val records: List[String] = List("4500", "7700", "9900", "21", "oops") +val lifetimes: List[Int] = List(3, 4, 2, 4, 3) val withLifetime: List[(String, Int)] = records.zip(lifetimes) def decodeInt(s: String): Either[Throwable, Int] = Either.catchNonFatal(s.toInt) +def fillErrorsAt0(e: Either[Throwable, Int]): Int = e.getOrElse(0) + +def calculateContributionPerMonth(balance: Int, lifetime: Int) = balance / lifetime + +val withDecodedBalance = withLifetime.map(_.leftMap(decodeInt)) + val result: List[Int] = - withLifetime.map( - _.leftMap(decodeInt) - ).map( - _.bimap({ - case Right(v) => v - case Left(_) => 0 - }, + withDecodedBalance.map( + _.bimap( + fillErrorsAt0, years => 12 * years - )).map({ - case (balance, lifetime) => balance / lifetime - }) + ) + ).map((calculateContributionPerMonth _).tupled) ``` 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. From a2bc671862983fa20145e5cf1bc14baad73108ba Mon Sep 17 00:00:00 2001 From: Stephen Lazaro Date: Fri, 19 Oct 2018 13:54:09 -0700 Subject: [PATCH 4/4] Simplify example --- docs/src/main/tut/typeclasses/bifunctor.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/docs/src/main/tut/typeclasses/bifunctor.md b/docs/src/main/tut/typeclasses/bifunctor.md index 807e722501..0e60f920bb 100644 --- a/docs/src/main/tut/typeclasses/bifunctor.md +++ b/docs/src/main/tut/typeclasses/bifunctor.md @@ -55,27 +55,18 @@ There is no `rightMap` however - use `map` instead. The reasoning behind this is 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. -A bit contrived, but we want an average contribution per month to the given account. -The list of balances is given as a list of numeric strings (except when they aren't), and the account lifetime is given in years. +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. ```tut:book -val records: List[String] = List("4500", "7700", "9900", "21", "oops") -val lifetimes: List[Int] = List(3, 4, 2, 4, 3) -val withLifetime: List[(String, Int)] = records.zip(lifetimes) - -def decodeInt(s: String): Either[Throwable, Int] = Either.catchNonFatal(s.toInt) - -def fillErrorsAt0(e: Either[Throwable, Int]): Int = e.getOrElse(0) +val records: List[(Int, Int)] = List((450000, 3), (770000, 4), (990000, 2), (2100, 4), (43300, 3)) def calculateContributionPerMonth(balance: Int, lifetime: Int) = balance / lifetime -val withDecodedBalance = withLifetime.map(_.leftMap(decodeInt)) - val result: List[Int] = - withDecodedBalance.map( - _.bimap( - fillErrorsAt0, + records.map( + record => record.bimap( + cents => cents / 100, years => 12 * years ) ).map((calculateContributionPerMonth _).tupled)