Skip to content

Add Boolean.otherwise and AnyF.withFilter for MonadError for-comprehension guards#609

Closed
torbenfreise wants to merge 2 commits intotypelevel:mainfrom
torbenfreise:add-withFilter-for-MonadError
Closed

Add Boolean.otherwise and AnyF.withFilter for MonadError for-comprehension guards#609
torbenfreise wants to merge 2 commits intotypelevel:mainfrom
torbenfreise:add-withFilter-for-MonadError

Conversation

@torbenfreise
Copy link
Copy Markdown

Adds two methods:
otherwise on BooleanOps - raises an error in any ApplicativeError context if the boolean is false.

withFilter on AnyFOps - enables filtering on MonadError types like Either.

These enable the use of typed 'if' guards in for-comprehensions over MonadError types, which is the main motivator behind this PR. For instance, instead of writing:

    for {
      age <- parseAge(age)
      _   <- F.raiseUnless(age >= 18)("age must be at least 18")
    } yield age

We can write:

    for {
      age <- parseAge(age)
      if (age >= 18) otherwise "Age must be at least 18"
    } yield age

Add otherwise to boolean
@benhutchison
Copy link
Copy Markdown
Member

Thanks for your contribution @torbenfreise

I have one request: the two changes work together to enable this cute syntax in for-expr, so please include a unit test that uses them in for-expression mode. That proves that this withFilter has the form the Scala compiler expects, and also adds an example of intended usage to the codebase. Other than that, 👍

@torbenfreise torbenfreise force-pushed the add-withFilter-for-MonadError branch 2 times, most recently from 7e9d24b to 831b378 Compare April 12, 2026 10:38
@torbenfreise torbenfreise force-pushed the add-withFilter-for-MonadError branch from 831b378 to f5b5c68 Compare April 12, 2026 10:39
@torbenfreise
Copy link
Copy Markdown
Author

torbenfreise commented Apr 12, 2026

@benhutchison thanks for your review, I added the test case.
I wasn't sure whether you wanted it to be in a separate test file (since it tests syntax from two different files) or if it should be in AnyFSyntaxTest.
I opted for the latter, let me know if this is correct.

While writing this test case, I also noticed that scalafmt rewrites it as

if (age >= 18).otherwise("Age must be at least 18")

which makes the syntax a bit less cute unfortunately.
This could be avoided by whitelisting otherwise for the Infix rewrite rule in scala.conf, but I guess that its better to keep the formatting consistent, what do you think?

@satorg
Copy link
Copy Markdown

satorg commented Apr 12, 2026

Honestly, I feel concerned about both of the proposed extensions:

  • otherwise – infix methods are generally discouraged in Scala. Moreover, Scala3 moves towards explicit marking of infix methods (see Rules for Operators). The overall consensus was to use infix methods with alphanumeric names very sparingly with really strong justification.
  • withFilter should implement filtering semantics. Otherwise its usage can be really confusing, for example:
    // `Option` implements `withFilter` with the filtering semantics
    for {
      (a, b) <- (2, "two").some if a % 2 == 0
    } println(s"$a, $b")  
    However, if we try to use Either[Throwable, A] (which is a MonadError) with the proposed withFilter, it won't compile:
    for {
      (a, b) <- (2, "two").asRight[Throwable] if a % 2 == 0
    } println(s"$a, $b")

@benhutchison
Copy link
Copy Markdown
Member

I find @satorg second point quite compelling. withFilter is broadly supposed to narrow a result using a predicate, but here we have an effectful call A => F[Unit] and it seems confusing to view this as a "predicate", where not-erroring is "true"

def withFilter[E](f: A => F[Unit])(implicit F: MonadError[F, E]): F[A]

My bad for not raising this earlier.

@torbenfreise
Copy link
Copy Markdown
Author

torbenfreise commented Apr 12, 2026

Thanks for the feedback,
I concede that this implementation leads to some confusing behaviour. For the example @satorg gave, it's a limitation of implementing withFilter on MonadError types that you need to somehow provide the alternative, in this case using otherwise

The compiler also gives a very unhelpful error:

found   : Boolean 
required: Either[Throwable,Unit]

I also agree that it is unintuitive that a predicate should be an effect with error / unit replacing false / true as you said @benhutchison.

I guess for this to be done properly, it would require a change to how Scala desugars for comprehensions, so that if pred else zero would desugar to

for {
   ...
   if pred else zero  // desugars to withFilterOrElse(pred, zero) 
}

for {
   a <- 2.asRight[Throwable] if a % 2 == 0 
  // fails to compile with the normal 'cannot resolve symbol withFilter' error
}

filterOrElse already exists on Either, so withFilterOrElse could be implemented like Option.withFilter to return a wrapper WithFilterOrElse class

@torbenfreise
Copy link
Copy Markdown
Author

I created a SIP for it here in case you are interested.
If this is accepted, then this syntax could available for the MonadError typeclass by defining .withFilterOrElse on its types.

@torbenfreise torbenfreise deleted the add-withFilter-for-MonadError branch April 13, 2026 18:57
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.

4 participants