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
31 changes: 31 additions & 0 deletions core/src/main/scala/cats/NonEmptyAlternative.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,36 @@ trait NonEmptyAlternative[F[_]] extends Applicative[F] with SemigroupK[F] { self
*/
def appendK[A](fa: F[A], a: A): F[A] = combineK(fa, pure(a))

/**
* Lift `fa` from `F[A]` into `F[Option[A]]` by surfacing every value `fa`
* produces as `Some(a)` and combining (via `combineK`) with `pure(None)`,
* so the result always succeeds at least once. The additional `None`
* witnesses the possibility that `fa` produced no values.
*
* This is the standard `optional` combinator from parser-combinator
* libraries and matches Haskell's `Control.Applicative.optional`:
* `Just <$> fa <|> pure Nothing`. For non-deterministic instances such
* as `List`, `attemptOption` always appends an extra `None`, which is
* consistent with the laws even if it can look surprising at first.
*
* Example:
* {{{
* scala> NonEmptyAlternative[Option].attemptOption(Option(5))
* res0: Option[Option[Int]] = Some(Some(5))
*
* scala> NonEmptyAlternative[Option].attemptOption(Option.empty[Int])
* res1: Option[Option[Int]] = Some(None)
*
* scala> NonEmptyAlternative[List].attemptOption(List(1, 2, 3))
* res2: List[Option[Int]] = List(Some(1), Some(2), Some(3), None)
*
* scala> NonEmptyAlternative[List].attemptOption(List.empty[Int])
* res3: List[Option[Int]] = List(None)
* }}}
*/
def attemptOption[A](fa: F[A]): F[Option[A]] =
combineK(map(fa)((a: A) => Some(a): Option[A]), pure(Option.empty[A]))

override def compose[G[_]: Applicative]: NonEmptyAlternative[λ[α => F[G[α]]]] =
new ComposedNonEmptyAlternative[F, G] {
val F = self
Expand Down Expand Up @@ -75,6 +105,7 @@ object NonEmptyAlternative {
val typeClassInstance: TypeClassType
def prependK(a: A): F[A] = typeClassInstance.prependK[A](a, self)
def appendK(a: A): F[A] = typeClassInstance.appendK[A](self, a)
def attemptOption: F[Option[A]] = typeClassInstance.attemptOption[A](self)
}
trait AllOps[F[_], A] extends Ops[F, A] with Applicative.AllOps[F, A] with SemigroupK.AllOps[F, A] {
type TypeClassType <: NonEmptyAlternative[F]
Expand Down
3 changes: 3 additions & 0 deletions laws/src/main/scala/cats/laws/NonEmptyAlternativeLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ trait NonEmptyAlternativeLaws[F[_]] extends ApplicativeLaws[F] with SemigroupKLa
def nonEmptyAlternativeAppendKConsistentWithPureAndCombineK[A](fa: F[A], a: A): IsEq[F[A]] =
fa.appendK(a) <-> (fa <+> a.pure[F])

def nonEmptyAlternativeAttemptOptionConsistentWithCombineKAndPure[A](fa: F[A]): IsEq[F[Option[A]]] =
F.attemptOption(fa) <-> (fa.map(Some(_): Option[A]) <+> Option.empty[A].pure[F])

@deprecated("typo in the name, use nonEmptyAlternativePrependKConsistentWithPureAndCombineK instead", "2.14.0")
private[laws] def nonEmptyAlternativePrependKConsitentWithPureAndCombineK[A](fa: F[A], a: A): IsEq[F[A]] =
nonEmptyAlternativePrependKConsistentWithPureAndCombineK(fa, a)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ trait AlternativeTests[F[_]] extends NonEmptyAlternativeTests[F] with MonoidKTes
EqFA: Eq[F[A]],
EqFB: Eq[F[B]],
EqFC: Eq[F[C]],
EqFOA: Eq[F[Option[A]]],
EqFABC: Eq[F[(A, B, C)]],
iso: Isomorphisms[F]
): RuleSet =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ trait NonEmptyAlternativeTests[F[_]] extends ApplicativeTests[F] with SemigroupK
EqFA: Eq[F[A]],
EqFB: Eq[F[B]],
EqFC: Eq[F[C]],
EqFOA: Eq[F[Option[A]]],
EqFABC: Eq[F[(A, B, C)]],
iso: Isomorphisms[F]
): RuleSet =
Expand All @@ -55,7 +56,9 @@ trait NonEmptyAlternativeTests[F[_]] extends ApplicativeTests[F] with SemigroupK
"prependK consistent with pure and combineK" ->
forAll(laws.nonEmptyAlternativePrependKConsistentWithPureAndCombineK[A] _),
"appendK consistent with pure and combineK" ->
forAll(laws.nonEmptyAlternativeAppendKConsistentWithPureAndCombineK[A] _)
forAll(laws.nonEmptyAlternativeAppendKConsistentWithPureAndCombineK[A] _),
"attemptOption consistent with combineK and pure" ->
forAll(laws.nonEmptyAlternativeAttemptOptionConsistentWithCombineKAndPure[A] _)
)
}
}
Expand Down
8 changes: 8 additions & 0 deletions mima.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,13 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq.concat(
ProblemFilters.exclude[MissingTypesProblem]("cats.free.FreeFoldable"),
ProblemFilters.exclude[IncompatibleTemplateDefProblem]("cats.RepresentableBimonad"),
ProblemFilters.exclude[IncompatibleTemplateDefProblem]("cats.RepresentableMonad")
),
Seq( // PR#4862 (issue #2936): added EqFOA: Eq[F[Option[A]]] implicit param to
// (NonEmpty)AlternativeTests rule sets so the new attemptOption law can run.
// Test-helper signatures only; source-compatible since callers pass implicits.
ProblemFilters.exclude[DirectMissingMethodProblem]("cats.laws.discipline.AlternativeTests.alternative"),
ProblemFilters.exclude[DirectMissingMethodProblem](
"cats.laws.discipline.NonEmptyAlternativeTests.nonEmptyAlternative"
)
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ package cats.tests

import cats.NonEmptyAlternative
import cats.laws.discipline.NonEmptyAlternativeTests
import cats.syntax.eq.*
import org.scalacheck.Prop.*

class NonEmptyAlternativeSuite extends CatsSuite {
implicit val listWrapperNeAlternative: NonEmptyAlternative[ListWrapper] = ListWrapper.nonEmptyAlternative
Expand All @@ -42,4 +44,18 @@ class NonEmptyAlternativeSuite extends CatsSuite {
"compose ListWrapper[ListWrapper[Int]]",
NonEmptyAlternativeTests.composed[ListWrapper, ListWrapper].nonEmptyAlternative[Int, Int, Int]
)

property("attemptOption on List concatenates map(Some) with pure(None)") {
forAll { (xs: List[Int]) =>
val expected: List[Option[Int]] = xs.map(Some(_)) :+ None
assert(NonEmptyAlternative[List].attemptOption(xs) === expected)
}
}

property("attemptOption on Option preserves Some, surfaces empty as Some(None)") {
forAll { (o: Option[Int]) =>
val expected: Option[Option[Int]] = o.fold[Option[Option[Int]]](Some(None))(a => Some(Some(a)))
assert(NonEmptyAlternative[Option].attemptOption(o) === expected)
}
}
}
Loading