Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions core/src/main/scala/cats/Foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package cats

import scala.collection.mutable
import cats.kernel.CommutativeMonoid
import cats.data.NonEmptyList

import Foldable.{sentinel, Source}

Expand Down Expand Up @@ -960,6 +961,73 @@ 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
Comment thread
TheBugYouCantFix marked this conversation as resolved.
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
FF: Foldable[F],
FA: Alternative[F],
GM: Monad[G]
): G[NonEmptyList[F[A]]] = {
type Acc = NonEmptyList[F[A]]
type State = (Foldable.Source[A], Acc)

def step(state: State): G[Either[State, Acc]] =
(state._1.uncons, state._2) match {
case (Some((a, rest)), acc) =>
GM.map(f(a)) { shouldSplit =>
val nextAcc =
if (shouldSplit)
FA.empty[A] :: acc
else
NonEmptyList(FA.appendK(acc.head, a), acc.tail)

Left((rest.value, nextAcc))
}

case (_, acc) =>
GM.pure(Right(acc))
}

val state0: State = (Foldable.Source.fromFoldable(fa), NonEmptyList.one(FA.empty[A]))
GM.map(GM.tailRecM(state0)(step))(_.reverse)
}
}

object Foldable {
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/scala/cats/syntax/foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down Expand Up @@ -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)(FF, FA, G)
}

def sliding2(implicit F: Foldable[F]): List[(A, A)] =
F.sliding2(fa)
def sliding3(implicit F: Foldable[F]): List[(A, A, A)] =
Expand Down
27 changes: 27 additions & 0 deletions tests/shared/src/test/scala/cats/tests/FoldableSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading