From 39f9ec6b750bfb3af8d4e3b1ec701bd3a78038a6 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Wed, 22 Apr 2026 10:33:36 +0300 Subject: [PATCH 1/5] splitWhen & splitWhenM for Foldable --- core/src/main/scala/cats/Foldable.scala | 43 +++++++++++++++++++ .../src/main/scala/cats/syntax/foldable.scala | 8 ++++ .../test/scala/cats/tests/FoldableSuite.scala | 23 ++++++++++ 3 files changed, 74 insertions(+) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index d83c8bb550..b31856dc58 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -960,6 +960,49 @@ trait Foldable[F[_]] extends UnorderedFoldable[F] with FoldableNFunctions[F] { s import cats.instances.either.* partitionBifoldM[G, Either, A, B, C](fa)(f)(A, M, Bifoldable[Either]) } + + /** + * Split this Foldable into a List of Lists based on a predicate. + * The behaviour is aimed to be identical to that of haskell's `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Foldable + * scala> Foldable[List].splitWhen(List(1,1))(_ == 1) + * res1: List[List[Int]] = List(List(), List(), List()) + * scala> Foldable[List].splitWhen(Nil)(_ == 1) + * res2: List[List[Nothing]] = List(List()) + * scala> Foldable[List].splitWhen(List(1, 2, 3, 1, 4, 5))(_ == 1) + * res3: List[List[Int]] = List(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhen[A](fa: F[A])(f: A => Boolean): List[List[A]] = { + toList(fa).reverse.foldLeft(List.empty[A] :: Nil) { case (lst, e) => + if (f(e)) Nil :: lst else (e :: lst.head) :: lst.tail + } + } + + /** + * Split this Foldable into a List of Lists based on the effectufl predicate. Monadic version of `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Foldable, cats.Eval + * scala> Foldable[List].splitWhenM(List(1,1))(x => Eval.now(x == 1)).value + * res1: List[List[Int]] = List(List(), List(), List()) + * scala> Foldable[List].splitWhenM(List.empty[Int])(x => Eval.now(x == 1)).value + * res2: List[List[Int]] = List(List()) + * scala> Foldable[List].splitWhenM(List(1, 2, 3, 1, 4, 5))(x => Eval.now(x == 1)).value + * val res3: List[List[Int]] = List(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhenM[G[_], A](fa: F[A])(f: A => G[Boolean])(implicit M: Monad[G]): G[List[List[A]]] = { + toList(fa).reverse.foldLeft(M.pure(List.empty[A] :: Nil)) { case (acc, e) => + M.flatMap(acc) { case lst => + M.map(f(e))(if (_) Nil :: lst else (e :: lst.head) :: lst.tail) + } + } + } } object Foldable { diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index b70eb71674..eb3fe0f577 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -304,6 +304,14 @@ final class FoldableOps0[F[_], A](private val fa: F[A]) extends AnyVal { )(implicit A: Alternative[F], F: Foldable[F], M: Monad[G]): G[(F[B], F[C])] = F.partitionEitherM[G, A, B, C](fa)(f)(A, M) + def splitWhen(f: A => Boolean)(implicit F: Foldable[F]): List[List[A]] = { + F.splitWhen[A](fa)(f) + } + + def splitWhenM[G[_]](f: A => G[Boolean])(implicit F: Foldable[F], G: Monad[G]): G[List[List[A]]] = { + F.splitWhenM[G, A](fa)(f)(G) + } + def sliding2(implicit F: Foldable[F]): List[(A, A)] = F.sliding2(fa) def sliding3(implicit F: Foldable[F]): List[(A, A, A)] = diff --git a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala index 73bfd06d84..4c1ba0f4dc 100644 --- a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala @@ -333,6 +333,29 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit } } + test(s"Foldable[$name].splitWhen") { + forAll { (fa: F[Int]) => + val pred = (x: Int) => x > 0 + val res = fa.splitWhen(pred) + val expectedFiltered = iterator(fa).filterNot(pred).toList + val expectedSize = fa.size - expectedFiltered.size + 1 + assert(res.size.toLong === expectedSize) + assert(res.flatten === expectedFiltered) + } + } + + test(s"Foldable[$name].splitWhenM") { + forAll { (fa: F[Int]) => + val pred = (x: Int) => x > 0 + val predM = (x: Int) => Eval.now(pred(x)) + val res = fa.splitWhenM(predM) + val expectedFiltered = iterator(fa).filterNot(pred).toList + val expectedSize = fa.size - expectedFiltered.size + 1 + assert(res.value.size.toLong === expectedSize) + assert(res.value.flatten === expectedFiltered) + } + } + test(s"Foldable[$name].sliding2 consistent with List#sliding(2)") { forAll { (fi: F[Int]) => val n = 2 From adec14b02cf4865495ca6293c1c9b102f0912cfe Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Wed, 22 Apr 2026 15:10:10 +0300 Subject: [PATCH 2/5] move splitWhen to list --- core/src/main/scala/cats/Foldable.scala | 43 ----------------- .../src/main/scala/cats/syntax/foldable.scala | 8 ---- core/src/main/scala/cats/syntax/list.scala | 47 +++++++++++++++++++ .../test/scala/cats/tests/FoldableSuite.scala | 23 --------- .../src/test/scala/cats/tests/ListSuite.scala | 23 +++++++++ 5 files changed, 70 insertions(+), 74 deletions(-) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index b31856dc58..d83c8bb550 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -960,49 +960,6 @@ trait Foldable[F[_]] extends UnorderedFoldable[F] with FoldableNFunctions[F] { s import cats.instances.either.* partitionBifoldM[G, Either, A, B, C](fa)(f)(A, M, Bifoldable[Either]) } - - /** - * Split this Foldable into a List of Lists based on a predicate. - * The behaviour is aimed to be identical to that of haskell's `splitWhen` - * - * {{{ - * scala> import cats.syntax.all._, cats.Foldable - * scala> Foldable[List].splitWhen(List(1,1))(_ == 1) - * res1: List[List[Int]] = List(List(), List(), List()) - * scala> Foldable[List].splitWhen(Nil)(_ == 1) - * res2: List[List[Nothing]] = List(List()) - * scala> Foldable[List].splitWhen(List(1, 2, 3, 1, 4, 5))(_ == 1) - * res3: List[List[Int]] = List(List(), List(2, 3), List(4, 5)) - * }}} - */ - - def splitWhen[A](fa: F[A])(f: A => Boolean): List[List[A]] = { - toList(fa).reverse.foldLeft(List.empty[A] :: Nil) { case (lst, e) => - if (f(e)) Nil :: lst else (e :: lst.head) :: lst.tail - } - } - - /** - * Split this Foldable into a List of Lists based on the effectufl predicate. Monadic version of `splitWhen` - * - * {{{ - * scala> import cats.syntax.all._, cats.Foldable, cats.Eval - * scala> Foldable[List].splitWhenM(List(1,1))(x => Eval.now(x == 1)).value - * res1: List[List[Int]] = List(List(), List(), List()) - * scala> Foldable[List].splitWhenM(List.empty[Int])(x => Eval.now(x == 1)).value - * res2: List[List[Int]] = List(List()) - * scala> Foldable[List].splitWhenM(List(1, 2, 3, 1, 4, 5))(x => Eval.now(x == 1)).value - * val res3: List[List[Int]] = List(List(), List(2, 3), List(4, 5)) - * }}} - */ - - def splitWhenM[G[_], A](fa: F[A])(f: A => G[Boolean])(implicit M: Monad[G]): G[List[List[A]]] = { - toList(fa).reverse.foldLeft(M.pure(List.empty[A] :: Nil)) { case (acc, e) => - M.flatMap(acc) { case lst => - M.map(f(e))(if (_) Nil :: lst else (e :: lst.head) :: lst.tail) - } - } - } } object Foldable { diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index eb3fe0f577..b70eb71674 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -304,14 +304,6 @@ final class FoldableOps0[F[_], A](private val fa: F[A]) extends AnyVal { )(implicit A: Alternative[F], F: Foldable[F], M: Monad[G]): G[(F[B], F[C])] = F.partitionEitherM[G, A, B, C](fa)(f)(A, M) - def splitWhen(f: A => Boolean)(implicit F: Foldable[F]): List[List[A]] = { - F.splitWhen[A](fa)(f) - } - - def splitWhenM[G[_]](f: A => G[Boolean])(implicit F: Foldable[F], G: Monad[G]): G[List[List[A]]] = { - F.splitWhenM[G, A](fa)(f)(G) - } - def sliding2(implicit F: Foldable[F]): List[(A, A)] = F.sliding2(fa) def sliding3(implicit F: Foldable[F]): List[(A, A, A)] = diff --git a/core/src/main/scala/cats/syntax/list.scala b/core/src/main/scala/cats/syntax/list.scala index bbbc635795..747c27d8c8 100644 --- a/core/src/main/scala/cats/syntax/list.scala +++ b/core/src/main/scala/cats/syntax/list.scala @@ -139,6 +139,53 @@ final class ListOps[A](private val la: List[A]) extends AnyVal { */ def scanRightNel[B](b: B)(f: (A, B) => B): NonEmptyList[B] = NonEmptyList.fromListUnsafe(la.scanRight(b)(f)) + + /** + * Split this List into a NonEmptyList of Lists based on a predicate. + * The behaviour is aimed to be identical to that of haskell's `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._ + * scala> List(1, 1).splitWhen(_ == 1) + * val res0: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) + * + * scala> List.empty[Int].splitWhen(_ == 1) + * val res1: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List()) + * + * scala> List(1, 2, 3, 1, 4, 5).splitWhen(_ == 1) + * val res2: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhen(f: A => Boolean): NonEmptyList[List[A]] = { + la.reverse.foldLeft(NonEmptyList.one(List.empty[A])) { case (lst, e) => + if (f(e)) Nil :: lst else NonEmptyList(e :: lst.head, lst.tail) + } + } + + /** + * Split this List into a NonEmptyList of Lists based on the effectufl predicate. Monadic version of `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Eval + * scala> List(1, 1).splitWhenM(x => Eval.now(x == 1)).value + * val res0: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) + * + * scala> List.empty[Int].splitWhenM(x => Eval.now(x == 1)).value + * val res1: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List()) + * + * scala> List(1, 2, 3, 1, 4, 5).splitWhenM(x => Eval.now(x == 1)).value + * val res2: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhenM[G[_]](f: A => G[Boolean])(implicit M: Monad[G]): G[NonEmptyList[List[A]]] = { + la.reverse.foldLeft(M.pure(NonEmptyList.one(List.empty[A]))) { case (acc, e) => + M.flatMap(acc) { case lst => + M.map(f(e))(if (_) Nil :: lst else NonEmptyList(e :: lst.head, lst.tail)) + } + } + } } private[syntax] trait ListSyntaxBinCompat0 { diff --git a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala index 4c1ba0f4dc..73bfd06d84 100644 --- a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala @@ -333,29 +333,6 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit } } - test(s"Foldable[$name].splitWhen") { - forAll { (fa: F[Int]) => - val pred = (x: Int) => x > 0 - val res = fa.splitWhen(pred) - val expectedFiltered = iterator(fa).filterNot(pred).toList - val expectedSize = fa.size - expectedFiltered.size + 1 - assert(res.size.toLong === expectedSize) - assert(res.flatten === expectedFiltered) - } - } - - test(s"Foldable[$name].splitWhenM") { - forAll { (fa: F[Int]) => - val pred = (x: Int) => x > 0 - val predM = (x: Int) => Eval.now(pred(x)) - val res = fa.splitWhenM(predM) - val expectedFiltered = iterator(fa).filterNot(pred).toList - val expectedSize = fa.size - expectedFiltered.size + 1 - assert(res.value.size.toLong === expectedSize) - assert(res.value.flatten === expectedFiltered) - } - } - test(s"Foldable[$name].sliding2 consistent with List#sliding(2)") { forAll { (fi: F[Int]) => val n = 2 diff --git a/tests/shared/src/test/scala/cats/tests/ListSuite.scala b/tests/shared/src/test/scala/cats/tests/ListSuite.scala index f2c10310f2..8d4bf90a5a 100644 --- a/tests/shared/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ListSuite.scala @@ -137,6 +137,29 @@ class ListSuite extends CatsSuite { assert(sumAll == lst.sum) } + + test(s"splitWhen") { + forAll { (li: List[Int]) => + val pred = (x: Int) => x > 0 + val res = li.splitWhen(pred) + val expectedFiltered = li.filterNot(pred) + val expectedSize = li.size - expectedFiltered.size + 1 + assert(res.size === expectedSize) + assert(res.toList.flatten === expectedFiltered) + } + } + + test(s"splitWhenM") { + forAll { (li: List[Int]) => + val pred = (x: Int) => x > 0 + val predM = (x: Int) => Eval.now(pred(x)) + val res = li.splitWhenM(predM) + val expectedFiltered = li.filterNot(pred) + val expectedSize = li.size - expectedFiltered.size + 1 + assert(res.value.size === expectedSize) + assert(res.value.toList.flatten === expectedFiltered) + } + } } final class ListInstancesSuite extends munit.FunSuite { From 44a9ea14ac07ffacefa26e32d95b127b1438bf5d Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Wed, 22 Apr 2026 21:48:26 +0300 Subject: [PATCH 3/5] optimize implementations --- core/src/main/scala/cats/syntax/list.scala | 22 +++++++++++++++---- .../src/test/scala/cats/tests/ListSuite.scala | 4 ++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/cats/syntax/list.scala b/core/src/main/scala/cats/syntax/list.scala index 747c27d8c8..ad97476d0c 100644 --- a/core/src/main/scala/cats/syntax/list.scala +++ b/core/src/main/scala/cats/syntax/list.scala @@ -180,12 +180,26 @@ final class ListOps[A](private val la: List[A]) extends AnyVal { */ def splitWhenM[G[_]](f: A => G[Boolean])(implicit M: Monad[G]): G[NonEmptyList[List[A]]] = { - la.reverse.foldLeft(M.pure(NonEmptyList.one(List.empty[A]))) { case (acc, e) => - M.flatMap(acc) { case lst => - M.map(f(e))(if (_) Nil :: lst else NonEmptyList(e :: lst.head, lst.tail)) + type State = (List[A], NonEmptyList[List[A]]) + + def step(state: State): G[Either[State, NonEmptyList[List[A]]]] = + state match { + case (e :: rest, acc) => + M.map(f(e)) { shouldSplit => + val nextAcc = + if (shouldSplit) Nil :: acc + else NonEmptyList(e :: acc.head, acc.tail) + + Left((rest, nextAcc)) + } + + case (Nil, acc) => + M.pure(Right(acc)) } - } + + M.tailRecM((la.reverse, NonEmptyList.one(List.empty[A])))(step) } + } private[syntax] trait ListSyntaxBinCompat0 { diff --git a/tests/shared/src/test/scala/cats/tests/ListSuite.scala b/tests/shared/src/test/scala/cats/tests/ListSuite.scala index 8d4bf90a5a..8214e4cb28 100644 --- a/tests/shared/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ListSuite.scala @@ -138,7 +138,7 @@ class ListSuite extends CatsSuite { assert(sumAll == lst.sum) } - test(s"splitWhen") { + test("splitWhen") { forAll { (li: List[Int]) => val pred = (x: Int) => x > 0 val res = li.splitWhen(pred) @@ -149,7 +149,7 @@ class ListSuite extends CatsSuite { } } - test(s"splitWhenM") { + test("splitWhenM") { forAll { (li: List[Int]) => val pred = (x: Int) => x > 0 val predM = (x: Int) => Eval.now(pred(x)) From 101241871b94d04188bb5c8239997f405a706405 Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Thu, 23 Apr 2026 02:37:30 +0300 Subject: [PATCH 4/5] additional test check --- .../src/test/scala/cats/tests/ListSuite.scala | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/shared/src/test/scala/cats/tests/ListSuite.scala b/tests/shared/src/test/scala/cats/tests/ListSuite.scala index 8214e4cb28..42b8d7d18e 100644 --- a/tests/shared/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ListSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Eval, Functor, Monad, Semigroupal, Traverse, TraverseFilter} +import cats.{Align, Alternative, CoflatMap, Eval, Functor, Monad, Reducible, Semigroupal, Traverse, TraverseFilter} import cats.data.{NonEmptyList, ZipList} import cats.laws.discipline.{ AlignTests, @@ -139,25 +139,27 @@ class ListSuite extends CatsSuite { } test("splitWhen") { - forAll { (li: List[Int]) => - val pred = (x: Int) => x > 0 + forAll { (li: List[Int], x: Int) => + val pred = (y: Int) => x == y val res = li.splitWhen(pred) val expectedFiltered = li.filterNot(pred) val expectedSize = li.size - expectedFiltered.size + 1 assert(res.size === expectedSize) assert(res.toList.flatten === expectedFiltered) + assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) } } test("splitWhenM") { - forAll { (li: List[Int]) => - val pred = (x: Int) => x > 0 - val predM = (x: Int) => Eval.now(pred(x)) - val res = li.splitWhenM(predM) + forAll { (li: List[Int], x: Int) => + val pred = (y: Int) => x == y + val predM = (y: Int) => Eval.now(pred(y)) + val res = li.splitWhenM(predM).value val expectedFiltered = li.filterNot(pred) val expectedSize = li.size - expectedFiltered.size + 1 - assert(res.value.size === expectedSize) - assert(res.value.toList.flatten === expectedFiltered) + assert(res.size === expectedSize) + assert(res.toList.flatten === expectedFiltered) + assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) } } } From 665d7e611f7fc71378da92705504a455c270f4ef Mon Sep 17 00:00:00 2001 From: TheBugYouCantFix Date: Thu, 23 Apr 2026 17:15:36 +0300 Subject: [PATCH 5/5] move splitWhen back to Foldable --- core/src/main/scala/cats/Foldable.scala | 55 +++++++++++++++++ .../src/main/scala/cats/syntax/foldable.scala | 12 ++++ core/src/main/scala/cats/syntax/list.scala | 61 ------------------- .../test/scala/cats/tests/FoldableSuite.scala | 27 ++++++++ .../src/test/scala/cats/tests/ListSuite.scala | 27 +------- 5 files changed, 95 insertions(+), 87 deletions(-) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index d83c8bb550..1206c628b3 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -23,6 +23,7 @@ package cats import scala.collection.mutable import cats.kernel.CommutativeMonoid +import cats.data.NonEmptyList import Foldable.{sentinel, Source} @@ -960,6 +961,60 @@ trait Foldable[F[_]] extends UnorderedFoldable[F] with FoldableNFunctions[F] { s import cats.instances.either.* partitionBifoldM[G, Either, A, B, C](fa)(f)(A, M, Bifoldable[Either]) } + + /** + * Split this Foldable into a NonEmptyList of Lists based on a predicate. + * The behaviour is aimed to be identical to that of haskell's `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Foldable, cats.data.NonEmptyList + * scala> Foldable[List].splitWhen(List(1,1))(_ == 1) + * res0: NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) + * scala> Foldable[List].splitWhen(Nil)(_ == 1) + * res1: NonEmptyList[List[Nothing]] = NonEmptyList(List()) + * scala> Foldable[List].splitWhen(List(1, 2, 3, 1, 4, 5))(_ == 1) + * res2: NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhen[A](fa: F[A])(f: A => Boolean)(implicit + FA: Alternative[F] + ): NonEmptyList[F[A]] = { + foldRight(fa, Eval.now(NonEmptyList.one(FA.empty[A]))) { + case (a, acc) if f(a) => acc.map(FA.empty[A] :: _) + case (a, acc) => acc.map(nel => NonEmptyList(FA.prependK(a, nel.head), nel.tail)) + }.value + } + + /** + * Split this Foldable into a NonEmptyList of Lists based on the effectufl predicate. Monadic version of `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Foldable, cats.Eval, cats.data.NonEmptyList + * scala> Foldable[List].splitWhenM(List(1,1))(x => Eval.now(x == 1)).value + * res0: NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) + * scala> Foldable[List].splitWhenM(List.empty[Int])(x => Eval.now(x == 1)).value + * res1: NonEmptyList[List[Int]] = NonEmptyList(List()) + * scala> Foldable[List].splitWhenM(List(1, 2, 3, 1, 4, 5))(x => Eval.now(x == 1)).value + * val res2: NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhenM[G[_], A](fa: F[A])(f: A => G[Boolean])(implicit + M: Monad[G], + FA: Alternative[F] + ): G[NonEmptyList[F[A]]] = { + foldRight(fa, Eval.now(M.pure(NonEmptyList.one(FA.empty[A])))) { (a, evalGnel) => + evalGnel.map { gnel => + M.flatMap(f(a)) { isDelimiter => + M.map(gnel) { nel => + if (isDelimiter) FA.empty[A] :: nel + else NonEmptyList(FA.prependK(a, nel.head), nel.tail) + } + } + } + }.value + } } object Foldable { diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index b70eb71674..a2e7ac3a21 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -22,6 +22,8 @@ package cats package syntax +import cats.data.NonEmptyList + trait FoldableSyntax extends Foldable.ToFoldableOps with UnorderedFoldable.ToUnorderedFoldableOps { implicit final def catsSyntaxNestedFoldable[F[_]: Foldable, G[_], A](fga: F[G[A]]): NestedFoldableOps[F, G, A] = @@ -304,6 +306,16 @@ final class FoldableOps0[F[_], A](private val fa: F[A]) extends AnyVal { )(implicit A: Alternative[F], F: Foldable[F], M: Monad[G]): G[(F[B], F[C])] = F.partitionEitherM[G, A, B, C](fa)(f)(A, M) + def splitWhen(f: A => Boolean)(implicit FF: Foldable[F], FA: Alternative[F]): NonEmptyList[F[A]] = { + FF.splitWhen[A](fa)(f)(FA) + } + + def splitWhenM[G[_]]( + f: A => G[Boolean] + )(implicit FF: Foldable[F], FA: Alternative[F], G: Monad[G]): G[NonEmptyList[F[A]]] = { + FF.splitWhenM[G, A](fa)(f)(G, FA) + } + def sliding2(implicit F: Foldable[F]): List[(A, A)] = F.sliding2(fa) def sliding3(implicit F: Foldable[F]): List[(A, A, A)] = diff --git a/core/src/main/scala/cats/syntax/list.scala b/core/src/main/scala/cats/syntax/list.scala index ad97476d0c..bbbc635795 100644 --- a/core/src/main/scala/cats/syntax/list.scala +++ b/core/src/main/scala/cats/syntax/list.scala @@ -139,67 +139,6 @@ final class ListOps[A](private val la: List[A]) extends AnyVal { */ def scanRightNel[B](b: B)(f: (A, B) => B): NonEmptyList[B] = NonEmptyList.fromListUnsafe(la.scanRight(b)(f)) - - /** - * Split this List into a NonEmptyList of Lists based on a predicate. - * The behaviour is aimed to be identical to that of haskell's `splitWhen` - * - * {{{ - * scala> import cats.syntax.all._ - * scala> List(1, 1).splitWhen(_ == 1) - * val res0: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) - * - * scala> List.empty[Int].splitWhen(_ == 1) - * val res1: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List()) - * - * scala> List(1, 2, 3, 1, 4, 5).splitWhen(_ == 1) - * val res2: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) - * }}} - */ - - def splitWhen(f: A => Boolean): NonEmptyList[List[A]] = { - la.reverse.foldLeft(NonEmptyList.one(List.empty[A])) { case (lst, e) => - if (f(e)) Nil :: lst else NonEmptyList(e :: lst.head, lst.tail) - } - } - - /** - * Split this List into a NonEmptyList of Lists based on the effectufl predicate. Monadic version of `splitWhen` - * - * {{{ - * scala> import cats.syntax.all._, cats.Eval - * scala> List(1, 1).splitWhenM(x => Eval.now(x == 1)).value - * val res0: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) - * - * scala> List.empty[Int].splitWhenM(x => Eval.now(x == 1)).value - * val res1: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List()) - * - * scala> List(1, 2, 3, 1, 4, 5).splitWhenM(x => Eval.now(x == 1)).value - * val res2: cats.data.NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) - * }}} - */ - - def splitWhenM[G[_]](f: A => G[Boolean])(implicit M: Monad[G]): G[NonEmptyList[List[A]]] = { - type State = (List[A], NonEmptyList[List[A]]) - - def step(state: State): G[Either[State, NonEmptyList[List[A]]]] = - state match { - case (e :: rest, acc) => - M.map(f(e)) { shouldSplit => - val nextAcc = - if (shouldSplit) Nil :: acc - else NonEmptyList(e :: acc.head, acc.tail) - - Left((rest, nextAcc)) - } - - case (Nil, acc) => - M.pure(Right(acc)) - } - - M.tailRecM((la.reverse, NonEmptyList.one(List.empty[A])))(step) - } - } private[syntax] trait ListSyntaxBinCompat0 { diff --git a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala index 73bfd06d84..2dbba313a2 100644 --- a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala @@ -333,6 +333,33 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit } } + test(s"Foldable[$name].splitWhen") { + forAll { (fi: F[Int], x: Int) => + val pred = (y: Int) => x == y + val li = fi.toList + val res = li.splitWhen(pred) + val expectedFiltered = li.filterNot(pred) + val expectedSize = li.size - expectedFiltered.size + 1 + assert(res.size === expectedSize) + assert(res.reduce === expectedFiltered) + assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) + } + } + + test(s"Foldable[$name].splitWhenM") { + forAll { (fi: F[Int], x: Int) => + val pred = (y: Int) => x == y + val predM = (y: Int) => Eval.now(pred(y)) + val li = fi.toList + val res = li.splitWhenM(predM).value + val expectedFiltered = li.filterNot(pred) + val expectedSize = li.size - expectedFiltered.size + 1 + assert(res.size === expectedSize) + assert(res.reduce === expectedFiltered) + assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) + } + } + test(s"Foldable[$name].sliding2 consistent with List#sliding(2)") { forAll { (fi: F[Int]) => val n = 2 diff --git a/tests/shared/src/test/scala/cats/tests/ListSuite.scala b/tests/shared/src/test/scala/cats/tests/ListSuite.scala index 42b8d7d18e..f2c10310f2 100644 --- a/tests/shared/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ListSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Eval, Functor, Monad, Reducible, Semigroupal, Traverse, TraverseFilter} +import cats.{Align, Alternative, CoflatMap, Eval, Functor, Monad, Semigroupal, Traverse, TraverseFilter} import cats.data.{NonEmptyList, ZipList} import cats.laws.discipline.{ AlignTests, @@ -137,31 +137,6 @@ class ListSuite extends CatsSuite { assert(sumAll == lst.sum) } - - test("splitWhen") { - forAll { (li: List[Int], x: Int) => - val pred = (y: Int) => x == y - val res = li.splitWhen(pred) - val expectedFiltered = li.filterNot(pred) - val expectedSize = li.size - expectedFiltered.size + 1 - assert(res.size === expectedSize) - assert(res.toList.flatten === expectedFiltered) - assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) - } - } - - test("splitWhenM") { - forAll { (li: List[Int], x: Int) => - val pred = (y: Int) => x == y - val predM = (y: Int) => Eval.now(pred(y)) - val res = li.splitWhenM(predM).value - val expectedFiltered = li.filterNot(pred) - val expectedSize = li.size - expectedFiltered.size + 1 - assert(res.size === expectedSize) - assert(res.toList.flatten === expectedFiltered) - assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) - } - } } final class ListInstancesSuite extends munit.FunSuite {