Skip to content

Add attemptOption to Alternative#4862

Open
MavenRain wants to merge 1 commit into
typelevel:mainfrom
MavenRain:attempt-option
Open

Add attemptOption to Alternative#4862
MavenRain wants to merge 1 commit into
typelevel:mainfrom
MavenRain:attempt-option

Conversation

@MavenRain
Copy link
Copy Markdown

@MavenRain MavenRain commented May 16, 2026

Closes #2936.

Adds attemptOption[A](fa: F[A]): F[Option[A]] to Alternative,
implementing the standard optional parser-combinator (Haskell's
Control.Applicative.optional): try fa, surface its result as
Some, and combine with pure(None) so the result always succeeds at
least once.

def attemptOption[A](fa: F[A]): F[Option[A]] =
  combineK(map(fa)((a: A) => Some(a): Option[A]), pure(Option.empty[A]))

  Implements the standard `optional` parser-combinator (Haskell's
  `Control.Applicative.optional`): lift a possibly-empty F[A] into an
  always-non-empty F[Option[A]] by combining `map(_.some)` with
  `pure(None)` via `combineK`.  Added to the `Alternative` trait with a
  default implementation and to the simulacrum-style `Ops` trait so it
  is reachable via `import cats.syntax.alternative._`.

  Closes typelevel#2936.

Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
@satorg satorg requested review from LukaJCB and satorg May 16, 2026 15:27
@satorg
Copy link
Copy Markdown
Contributor

satorg commented May 16, 2026

@MavenRain , thank you for the PR!

I've got a few comments:

  1. It can be moved to NonEmptyAlternative.
  2. It can use appendK instead of combineK.
  3. It makes sense to add a corresponding rule to NonEmptyAlternativeLaws using the default implementation in the rule body.
  4. Does it have to be *Option only? Does it make sense to make it more generic, e.g.:
    def attemptG[G[_] : Alternative, A]: F[G[A]] = ???
    However, if it does make sense, then it can benefit from the partial-apply technique, so that we could write something like this:
    scala> List(1, 2, 3).attemtG[Option]
    res0: List[Option[Int]] = List(Some(1), Some(2), Some(3), None)
  5. Why attempt name in the first place? Cats already uses attempt for seemingly different purposes.

Comment on lines +89 to +94
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))
}
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.

* }}}
*/
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add attemptOption to Alternative

3 participants