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/Alternative.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,36 @@ trait Alternative[F[_]] extends NonEmptyAlternative[F] with MonoidK[F] { self =>
def guard(condition: Boolean): F[Unit] =
if (condition) unit else empty

/**
* Lift `fa` from `F[A]` into `F[Option[A]]` by surfacing every `a` that `fa` produces as
* `Some(a)` and combining (via `combineK`) with a `pure(None)` so that `attemptOption(fa)`
* always succeeds at least once, with the additional `None` witnessing 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`. Note that for
* non-deterministic instances such as `List`, `attemptOption` always appends an extra
* `None`, which is consistent with the Alternative laws but may look surprising at first
* glance.
*
* Example:
* {{{
* scala> Alternative[Option].attemptOption(Option(5))
* res0: Option[Option[Int]] = Some(Some(5))
*
* scala> Alternative[Option].attemptOption(Option.empty[Int])
* res1: Option[Option[Int]] = Some(None)
*
* scala> Alternative[List].attemptOption(List(1, 2, 3))
* res2: List[Option[Int]] = List(Some(1), Some(2), Some(3), None)
*
* scala> Alternative[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]))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I'd rather add:

private val fempty: F[Option[Nothing]] = pure(Option.empty[Nothing])

def attemptOption[A](fa: F[A]): F[Option[A]] =
    combineK(map(fa)((a: A) => Some(a): Option[A]), widen[Option[A]](fempty))

Since widen by default is a cast (and generally 0 cost), this prevents allocating the fempty on every call.


override def compose[G[_]: Applicative]: Alternative[λ[α => F[G[α]]]] =
new ComposedAlternative[F, G] {
val F = self
Expand Down Expand Up @@ -158,6 +188,7 @@ object Alternative {
typeClassInstance.separate[G, B, C](self.asInstanceOf[F[G[B, C]]])
def separateFoldable[G[_, _], B, C](implicit ev$1: A <:< G[B, C], G: Bifoldable[G], FF: Foldable[F]): (F[B], F[C]) =
typeClassInstance.separateFoldable[G, B, C](self.asInstanceOf[F[G[B, C]]])(G, FF)
def attemptOption: F[Option[A]] = typeClassInstance.attemptOption[A](self)
}
trait AllOps[F[_], A] extends Ops[F, A] with NonEmptyAlternative.AllOps[F, A] with MonoidK.AllOps[F, A] {
type TypeClassType <: Alternative[F]
Expand Down
21 changes: 21 additions & 0 deletions tests/shared/src/test/scala/cats/tests/AlternativeSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,25 @@ class AlternativeSuite extends CatsSuite {
assert(Alternative[Option].guard(true).isDefined)
assert(Alternative[Option].guard(false).isEmpty)
}

test("attemptOption") {
assert(Alternative[Option].attemptOption(Option(5)) === Some(Some(5)))
assert(Alternative[Option].attemptOption(Option.empty[Int]) === Some(None))
assert(Alternative[List].attemptOption(List(1, 2, 3)) === List(Some(1), Some(2), Some(3), None))
assert(Alternative[List].attemptOption(List.empty[Int]) === List(None))
}
Comment on lines +89 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test doesn't seem providing any value because the following two property based tests should cover all these cases already.


property("attemptOption is map(Some) combineK pure(None) for List") {
forAll { (xs: List[Int]) =>
val expected: List[Option[Int]] = xs.map(Some(_)) :+ None
assert(Alternative[List].attemptOption(xs) === expected)
}
}

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