Skip to content

Commit

Permalink
Add Foldable.foldMapM and Reducible.reduceMapM (#1452)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterneyens authored and johnynek committed Jan 3, 2017
1 parent 87d1ef6 commit 5bcb62f
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 4 deletions.
21 changes: 21 additions & 0 deletions core/src/main/scala/cats/Foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,27 @@ import simulacrum.typeclass
def foldM[G[_], A, B](fa: F[A], z: B)(f: (B, A) => G[B])(implicit G: Monad[G]): G[B] =
foldLeft(fa, G.pure(z))((gb, a) => G.flatMap(gb)(f(_, a)))

/**
* Monadic folding on `F` by mapping `A` values to `G[B]`, combining the `B`
* values using the given `Monoid[B]` instance.
*
* Similar to [[foldM]], but using a `Monoid[B]`.
*
* {{{
* scala> import cats.Foldable
* scala> import cats.implicits._
* scala> val evenNumbers = List(2,4,6,8,10)
* scala> val evenOpt: Int => Option[Int] =
* | i => if (i % 2 == 0) Some(i) else None
* scala> Foldable[List].foldMapM(evenNumbers)(evenOpt)
* res0: Option[Int] = Some(30)
* scala> Foldable[List].foldMapM(evenNumbers :+ 11)(evenOpt)
* res1: Option[Int] = None
* }}}
*/
def foldMapM[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Monad[G], B: Monoid[B]): G[B] =
foldM(fa, B.empty)((b, a) => G.map(f(a))(B.combine(b, _)))

/**
* Traverse `F[A]` using `Applicative[G]`.
*
Expand Down
29 changes: 27 additions & 2 deletions core/src/main/scala/cats/Reducible.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,32 @@ import simulacrum.typeclass
reduceLeftTo(fa)(f)((gb, a) => G.flatMap(gb)(g(_, a)))

/**
* Overriden from Foldable[_] for efficiency.
* Monadic reducing by mapping the `A` values to `G[B]`. combining
* the `B` values using the given `Semigroup[B]` instance.
*
* Similar to [[reduceLeftM]], but using a `Semigroup[B]`.
*
* {{{
* scala> import cats.Reducible
* scala> import cats.data.NonEmptyList
* scala> import cats.implicits._
* scala> val evenOpt: Int => Option[Int] =
* | i => if (i % 2 == 0) Some(i) else None
* scala> val allEven = NonEmptyList.of(2,4,6,8,10)
* allEven: cats.data.NonEmptyList[Int] = NonEmptyList(2, 4, 6, 8, 10)
* scala> val notAllEven = allEven ++ List(11)
* notAllEven: cats.data.NonEmptyList[Int] = NonEmptyList(2, 4, 6, 8, 10, 11)
* scala> Reducible[NonEmptyList].reduceMapM(allEven)(evenOpt)
* res0: Option[Int] = Some(30)
* scala> Reducible[NonEmptyList].reduceMapM(notAllEven)(evenOpt)
* res1: Option[Int] = None
* }}}
*/
def reduceMapM[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: FlatMap[G], B: Semigroup[B]): G[B] =
reduceLeftM(fa)(f)((b, a) => G.map(f(a))(B.combine(b, _)))

/**
* Overriden from [[Foldable]] for efficiency.
*/
override def reduceLeftToOption[A, B](fa: F[A])(f: A => B)(g: (B, A) => B): Option[B] =
Some(reduceLeftTo(fa)(f)(g))
Expand All @@ -80,7 +105,7 @@ import simulacrum.typeclass
def reduceRightTo[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B]

/**
* Overriden from `Foldable[_]` for efficiency.
* Overriden from [[Foldable]] for efficiency.
*/
override def reduceRightToOption[A, B](fa: F[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[Option[B]] =
reduceRightTo(fa)(f)(g).map(Some(_))
Expand Down
10 changes: 8 additions & 2 deletions tests/src/test/scala/cats/tests/FoldableTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,16 @@ class FoldableTestsAdditional extends CatsSuite {
F.foldMap(names)(_.length) should === (names.map(_.length).sum)
val sumM = F.foldM(names, "") { (acc, x) => (Some(acc + x): Option[String]) }
assert(sumM == Some("AaronBettyCalvinDeirdra"))
val sumMapM = F.foldMapM(names) { x => (Some(x): Option[String]) }
assert(sumMapM == Some("AaronBettyCalvinDeirdra"))
val isNotCalvin: String => Option[String] =
x => if (x == "Calvin") None else Some(x)
val notCalvin = F.foldM(names, "") { (acc, x) =>
if (x == "Calvin") (None: Option[String])
else (Some(acc + x): Option[String]) }
isNotCalvin(x).map(acc + _)
}
assert(notCalvin == None)
val notCalvinMapM = F.foldMapM(names)(isNotCalvin)
assert(notCalvinMapM == None)

// test trampolining
val large = (1 to 10000).toList
Expand Down
16 changes: 16 additions & 0 deletions tests/src/test/scala/cats/tests/NonEmptyListTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,22 @@ class NonEmptyListTests extends CatsSuite {
}
}

test("reduceLeftM consistent with foldM") {
forAll { (nel: NonEmptyList[Int], f: Int => Option[Int]) =>
val got = nel.reduceLeftM(f)((acc, i) => f(i).map(acc + _))
val expected = f(nel.head).flatMap { hd =>
nel.tail.foldM(hd)((acc, i) => f(i).map(acc + _))
}
got should === (expected)
}
}

test("reduceMapM consistent with foldMapM") {
forAll { (nel: NonEmptyList[Int], f: Int => Option[Int]) =>
nel.reduceMapM(f) should === (nel.foldMapM(f))
}
}

test("fromList round trip") {
forAll { l: List[Int] =>
NonEmptyList.fromList(l).map(_.toList).getOrElse(List.empty) should === (l)
Expand Down
45 changes: 45 additions & 0 deletions tests/src/test/scala/cats/tests/ReducibleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package tests

import org.scalacheck.Arbitrary

import cats.data.NonEmptyList

class ReducibleTestsAdditional extends CatsSuite {

test("Reducible[NonEmptyList].reduceLeftM stack safety") {
Expand All @@ -15,6 +17,49 @@ class ReducibleTestsAdditional extends CatsSuite {
actual should === (Some(expected))
}

// exists method written in terms of reduceRightTo
def contains[F[_]: Reducible, A: Eq](as: F[A], goal: A): Eval[Boolean] =
as.reduceRightTo(_ === goal) { (a, lb) =>
if (a === goal) Now(true) else lb
}


test("Reducible[NonEmptyList]") {
val R = Reducible[NonEmptyList]

// some basic sanity checks
val tail = (2 to 10).toList
val total = 1 + tail.sum
val nel = NonEmptyList(1, tail)
R.reduceLeft(nel)(_ + _) should === (total)
R.reduceRight(nel)((x, ly) => ly.map(x + _)).value should === (total)
R.reduce(nel) should === (total)

// more basic checks
val names = NonEmptyList.of("Aaron", "Betty", "Calvin", "Deirdra")
val totalLength = names.toList.map(_.length).sum
R.reduceLeftTo(names)(_.length)((sum, s) => s.length + sum) should === (totalLength)
R.reduceMap(names)(_.length) should === (totalLength)
val sumLeftM = R.reduceLeftM(names)(Some(_): Option[String]) { (acc, x) =>
(Some(acc + x): Option[String])
}
assert(sumLeftM == Some("AaronBettyCalvinDeirdra"))
val sumMapM = R.reduceMapM(names) { x => (Some(x): Option[String]) }
assert(sumMapM == Some("AaronBettyCalvinDeirdra"))
val isNotCalvin: String => Option[String] =
x => if (x == "Calvin") None else Some(x)
val notCalvin = R.reduceLeftM(names)(isNotCalvin) { (acc, x) =>
isNotCalvin(x).map(acc + _)
}
assert(notCalvin == None)
val notCalvinMapM = R.reduceMapM(names)(isNotCalvin)
assert(notCalvinMapM == None)

// test trampolining
val large = NonEmptyList(1, (2 to 10000).toList)
assert(contains(large, 10000).value)
}

}

abstract class ReducibleCheck[F[_]: Reducible](name: String)(implicit ArbFInt: Arbitrary[F[Int]]) extends FoldableCheck[F](name) {
Expand Down

0 comments on commit 5bcb62f

Please sign in to comment.