diff --git a/core/src/main/scala-2.12/cats/compat/FoldableCompat.scala b/core/src/main/scala-2.12/cats/compat/FoldableCompat.scala new file mode 100644 index 0000000000..5775d527dc --- /dev/null +++ b/core/src/main/scala-2.12/cats/compat/FoldableCompat.scala @@ -0,0 +1,11 @@ +package cats +package compat + +private[cats] object FoldableCompat { + + def toIterable[F[_], A](fa: F[A])(F: Foldable[F]): Iterable[A] = + F.foldRight[A, Stream[A]](fa, Eval.now(Stream.empty)) { (a, eb) => + eb.map(Stream.cons(a, _)) + } + .value +} diff --git a/core/src/main/scala-2.12/cats/instances/stream.scala b/core/src/main/scala-2.12/cats/instances/stream.scala index 67d4db6e19..b222d3cf11 100644 --- a/core/src/main/scala-2.12/cats/instances/stream.scala +++ b/core/src/main/scala-2.12/cats/instances/stream.scala @@ -140,6 +140,8 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances { override def toList[A](fa: Stream[A]): List[A] = fa.toList + override def toIterable[A](fa: Stream[A]): Iterable[A] = fa + override def reduceLeftOption[A](fa: Stream[A])(f: (A, A) => A): Option[A] = fa.reduceLeftOption(f) diff --git a/core/src/main/scala-2.13+/cats/compat/FoldableCompat.scala b/core/src/main/scala-2.13+/cats/compat/FoldableCompat.scala new file mode 100644 index 0000000000..07c8abdadd --- /dev/null +++ b/core/src/main/scala-2.13+/cats/compat/FoldableCompat.scala @@ -0,0 +1,11 @@ +package cats +package compat + +private[cats] object FoldableCompat { + + def toIterable[F[_], A](fa: F[A])(F: Foldable[F]): Iterable[A] = + F.foldRight[A, LazyList[A]](fa, Eval.now(LazyList.empty)) { (a, eb) => + eb.map(LazyList.cons(a, _)) + } + .value +} diff --git a/core/src/main/scala-2.13+/cats/instances/lazyList.scala b/core/src/main/scala-2.13+/cats/instances/lazyList.scala index bf570f606a..22621a6e3d 100644 --- a/core/src/main/scala-2.13+/cats/instances/lazyList.scala +++ b/core/src/main/scala-2.13+/cats/instances/lazyList.scala @@ -118,6 +118,8 @@ trait LazyListInstances extends cats.kernel.instances.LazyListInstances { override def toList[A](fa: LazyList[A]): List[A] = fa.toList + override def toIterable[A](fa: LazyList[A]): Iterable[A] = fa + override def reduceLeftOption[A](fa: LazyList[A])(f: (A, A) => A): Option[A] = fa.reduceLeftOption(f) diff --git a/core/src/main/scala-2.13+/cats/instances/stream.scala b/core/src/main/scala-2.13+/cats/instances/stream.scala index 65e8f072ce..6874c1e4c3 100644 --- a/core/src/main/scala-2.13+/cats/instances/stream.scala +++ b/core/src/main/scala-2.13+/cats/instances/stream.scala @@ -141,6 +141,8 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances { override def toList[A](fa: Stream[A]): List[A] = fa.toList + override def toIterable[A](fa: Stream[A]): Iterable[A] = fa + override def reduceLeftOption[A](fa: Stream[A])(f: (A, A) => A): Option[A] = fa.reduceLeftOption(f) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index 0e2d8b69e5..8868a387a4 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -266,15 +266,25 @@ import Foldable.sentinel * Fold implemented using the given Monoid[A] instance. */ def fold[A](fa: F[A])(implicit A: Monoid[A]): A = - foldLeft(fa, A.empty) { (acc, a) => - A.combine(acc, a) - } + A.combineAll(toIterable(fa)) /** * Alias for [[fold]]. */ def combineAll[A: Monoid](fa: F[A]): A = fold(fa) + def combineAllOption[A](fa: F[A])(implicit ev: Semigroup[A]): Option[A] = + if (isEmpty(fa)) None else ev.combineAllOption(toIterable(fa)) + + /** + * Convert F[A] to an Iterable[A]. + * + * This method may be overridden for the sake of performance, but implementers should take care + * not to force a full materialization of the collection. + */ + def toIterable[A](fa: F[A]): Iterable[A] = + cats.compat.FoldableCompat.toIterable(fa)(self) + /** * Fold implemented by mapping `A` values into `B` and then * combining them using the given `Monoid[B]` instance. diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index 53157f0c99..c02c28b128 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -2,6 +2,7 @@ package cats package syntax trait FoldableSyntax extends Foldable.ToFoldableOps with UnorderedFoldable.ToUnorderedFoldableOps { + implicit final def catsSyntaxNestedFoldable[F[_]: Foldable, G[_], A](fga: F[G[A]]): NestedFoldableOps[F, G, A] = new NestedFoldableOps[F, G, A](fga) diff --git a/tests/src/test/scala/cats/tests/FoldableSuite.scala b/tests/src/test/scala/cats/tests/FoldableSuite.scala index addf449232..d383405b47 100644 --- a/tests/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/src/test/scala/cats/tests/FoldableSuite.scala @@ -214,6 +214,20 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit ArbFInt: Arb } } + test(s"Foldable[$name].combineAllOption") { + forAll { (fa: F[Int]) => + fa.combineAllOption should ===(fa.toList.combineAllOption) + fa.combineAllOption should ===(iterator(fa).toList.combineAllOption) + } + } + + test(s"Foldable[$name].iterable") { + forAll { (fa: F[Int]) => + fa.toIterable.toList should ===(fa.toList) + fa.toIterable.toList should ===(iterator(fa).toList) + } + } + test(s"Foldable[$name].intercalate") { forAll { (fa: F[String], a: String) => fa.intercalate(a) should ===(fa.toList.mkString(a))