Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 3141 #3150

Merged
merged 15 commits into from
Dec 10, 2019
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]]])
LukaJCB marked this conversation as resolved.
Show resolved Hide resolved

/**
* test if `F[A]` contains an `A`, named contains_ to avoid conflict with existing contains which uses universal equality
*
Expand Down
6 changes: 5 additions & 1 deletion core/src/main/scala/cats/syntax/reducible.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ final class ReducibleOps0[F[_], A](private val fa: F[A]) extends AnyVal {
* a: String = "foo321"
* }}}
* */
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 reduceMapK[G[_], B](f: A => G[B])(implicit F: Reducible[F], G: SemigroupK[G]): G[B] =
F.reduceLeftTo(fa)(f)((b, a) => G.combineK(b, f(a)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this? In general we want to avoid having implementation logic in syntax type classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my bad, I'll remove this diff.


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]] =
LukaJCB marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -77,6 +77,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 @@ -88,6 +89,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