Skip to content

Commit

Permalink
Issue 3141 (#3150)
Browse files Browse the repository at this point in the history
* #3141 [WIP] Implemented .foldA

* #3141 [WIP] Implemented .reduceA

* #3141 [WIP] Implemented .reduceMapA

* #3141 Refactored

* #3141 Added tests

* #3141 Fixed 2.13

* #3141 Added link to the issue at simulacrum

* #3141 Fixed docs

* #3141 Formatted

* #3141 Refactored

* #3141 Changed .reduceMap signature

* #3141 Removed noop on .reduceMapA

* #3141 Fixed docs

* #3141 Removed changes from reduceMapK
  • Loading branch information
Twizty authored and LukaJCB committed Dec 10, 2019
1 parent 8e4987f commit 70c9625
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 0 deletions.
20 changes: 20 additions & 0 deletions core/src/main/scala/cats/Foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,26 @@ import Foldable.sentinel
}
}

/**
* Fold implemented using the given `Applicative[G]` and `Monoid[A]` instance.
*
* This method is identical to fold, except that we use `Applicative[G]` and `Monoid[A]`
* to combine a's inside an applicative G.
*
* For example:
*
* {{{
* scala> import cats.implicits._
* scala> val F = Foldable[List]
* scala> F.foldA(List(Either.right[String, Int](1), Either.right[String, Int](2)))
* res0: Either[String, Int] = Right(3)
* }}}
*
* `noop` usage description [[https://github.com/typelevel/simulacrum/issues/162 here]]
*/
@noop def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[A] =
fold(fga)(Applicative.monoid)

/**
* Fold implemented by mapping `A` values into `B` in a context `G` and then
* combining them using the `MonoidK[G]` instance.
Expand Down
16 changes: 16 additions & 0 deletions core/src/main/scala/cats/Reducible.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ import simulacrum.{noop, typeclass}
def reduceLeftM[G[_], A, B](fa: F[A])(f: A => G[B])(g: (B, A) => G[B])(implicit G: FlatMap[G]): G[B] =
reduceLeftTo(fa)(f)((gb, a) => G.flatMap(gb)(g(_, a)))

/**
* Reduce a `F[G[A]]` value using `Applicative[G]` and `Semigroup[A]`, a universal
* semigroup for `G[_]`.
*
* `noop` usage description [[https://github.com/typelevel/simulacrum/issues/162 here]]
*/
@noop def reduceA[G[_], A](fga: F[G[A]])(implicit G: Apply[G], A: Semigroup[A]): G[A] =
reduce(fga)(Apply.semigroup)

/**
* Apply `f` to each `a` of `fa` and combine the result into Apply[G] using the
* given `Semigroup[B]`.
*/
def reduceMapA[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Apply[G], B: Semigroup[B]): G[B] =
reduceLeftTo(fa)(f)((gb, a) => G.map2(gb, f(a))(B.combine))

/**
* Monadic reducing by mapping the `A` values to `G[B]`. combining
* the `B` values using the given `Semigroup[B]` instance.
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/syntax/foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ final class FoldableOps[F[_], A](private val fa: F[A]) extends AnyVal {
def foldr[B](b: Eval[B])(f: (A, Eval[B]) => Eval[B])(implicit F: Foldable[F]): Eval[B] =
F.foldRight(fa, b)(f)

def foldA[G[_], B](implicit F: Foldable[F], ev: A <:< G[B], G: Applicative[G], B: Monoid[B]): G[B] =
F.foldA[G, B](fa.asInstanceOf[F[G[B]]])

/**
* test if `F[A]` contains an `A`, named contains_ to avoid conflict with existing contains which uses universal equality
*
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/syntax/reducible.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ final class ReducibleOps0[F[_], A](private val fa: F[A]) extends AnyVal {
* }}}
* */
def reduceMapK[G[_], B](f: A => G[B])(implicit F: Reducible[F], G: SemigroupK[G]): G[B] = F.reduceMapK[G, A, B](fa)(f)

def reduceA[G[_], B](implicit F: Reducible[F], ev: A <:< G[B], G: Apply[G], B: Semigroup[B]): G[B] =
F.reduceA[G, B](fa.asInstanceOf[F[G[B]]])
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,7 @@ class ReducibleNonEmptyStreamSuite extends ReducibleSuite[NonEmptyStream]("NonEm
val tailStart: Long = start + 1L
NonEmptyStream(start, tailStart.to(endInclusive).toStream)
}

def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyStream[Either[L, R]] =
NonEmptyStream(el, els: _*)
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,7 @@ class ReducibleNonEmptyLazyListSuite extends ReducibleSuite[NonEmptyLazyList]("N

def range(start: Long, endInclusive: Long): NonEmptyLazyList[Long] =
NonEmptyLazyList(start, (start + 1L).to(endInclusive): _*)

def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyLazyList[Either[L, R]] =
NonEmptyLazyList(el, els: _*)
}
14 changes: 14 additions & 0 deletions tests/src/test/scala/cats/tests/FoldableSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,20 @@ class FoldableSuiteAdditional extends CatsSuite with ScalaVersionSpecificFoldabl
Foldable[Stream].foldRight(fa, lb)(f)
}

test(".foldA successful case") {
implicit val F = foldableStreamWithDefaultImpl
val ns = Stream.apply[Either[String, Int]](1.asRight, 2.asRight, 7.asRight)

assert(F.foldA(ns) == 10.asRight[String])
}

test(".foldA failed case") {
implicit val F = foldableStreamWithDefaultImpl
val ns = Stream.apply[Either[String, Int]](1.asRight, "boom!!!".asLeft, 7.asRight)

assert(ns.foldA == "boom!!!".asLeft[Int])
}

test(".foldLeftM short-circuiting") {
implicit val F = foldableStreamWithDefaultImpl
val ns = Stream.continually(1)
Expand Down
3 changes: 3 additions & 0 deletions tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,7 @@ class ReducibleNonEmptyChainSuite extends ReducibleSuite[NonEmptyChain]("NonEmpt

def range(start: Long, endInclusive: Long): NonEmptyChain[Long] =
NonEmptyChain(start, (start + 1L).to(endInclusive): _*)

def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyChain[Either[L, R]] =
NonEmptyChain(el, els: _*)
}
2 changes: 2 additions & 0 deletions tests/src/test/scala/cats/tests/NonEmptyListSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,6 @@ class ReducibleNonEmptyListSuite extends ReducibleSuite[NonEmptyList]("NonEmptyL
NonEmptyList(start, (tailStart).to(endInclusive).toList)
}

def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyList[Either[L, R]] =
NonEmptyList(el, List(els: _*))
}
3 changes: 3 additions & 0 deletions tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,7 @@ class ReducibleNonEmptyVectorSuite extends ReducibleSuite[NonEmptyVector]("NonEm
val tailStart: Long = start + 1L
NonEmptyVector(start, (tailStart).to(endInclusive).toVector)
}

def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyVector[Either[L, R]] =
NonEmptyVector(el, Vector(els: _*))
}
28 changes: 28 additions & 0 deletions tests/src/test/scala/cats/tests/ReducibleSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ abstract class ReducibleSuite[F[_]: Reducible](name: String)(implicit ArbFInt: A
extends FoldableSuite[F](name) {

def range(start: Long, endInclusive: Long): F[Long]
def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): F[Either[L, R]]

test(s"Reducible[$name].reduceLeftM stack safety") {
def nonzero(acc: Long, x: Long): Option[Long] =
Expand All @@ -117,6 +118,33 @@ abstract class ReducibleSuite[F[_]: Reducible](name: String)(implicit ArbFInt: A
actual should ===(Some(expected))
}

test(s"Reducible[$name].reduceA successful case") {
val expected = 6
val actual = rangeE(1.asRight[String], 2.asRight[String], 3.asRight[String]).reduceA
actual should ===(expected.asRight[String])
}

test(s"Reducible[$name].reduceA failure case") {
val expected = "boom!!!"
val actual = rangeE(1.asRight, "boom!!!".asLeft, 3.asRight).reduceA
actual should ===(expected.asLeft[Int])
}

test(s"Reducible[$name].reduceMapA successful case") {
val expected = "123"
val actual = range(1, 3).reduceMapA(_.toString.some)

actual should ===(expected.some)
}

test(s"Reducible[$name].reduceMapA failure case") {
def intToString(i: Long): Either[String, Int] = if (i == 2) i.toInt.asRight else "boom!!!".asLeft

val expected = "boom!!!"
val actual = range(1, 3).reduceMapA(intToString)
actual should ===(expected.asLeft[Int])
}

test(s"Reducible[$name].toNonEmptyList/toList consistency") {
forAll { (fa: F[Int]) =>
fa.toList.toNel should ===(Some(fa.toNonEmptyList))
Expand Down

0 comments on commit 70c9625

Please sign in to comment.