Motivation
Either - if guards are unsupported
In for comprehensions over Either, there is no idiomatic way to short-circuit the expression to a Left if a predicate does not hold. The current way to do this in stdlib looks something like this:
for {
x <- getX
_ <- if (x % 2 == 0) Right(()) else Left("x must be even")
} yield x
This requires a dummy _ binding, a Right(()) to act as a ‘success sentinel’, which obfuscates the purpose of the expression and the predicate itself.
A clearer way to express this could look like:
for {
x <- getX
if x % 2 == 0 else "x must be even"
} yield x
Try - if guards produce non-descriptive failures
Guards are supported in for comprehensions over Try, as Try implements withFilter. So it is possible to write:
for{
x <- tryX
if x % 2 == 0
} yield x
However, they produce a generic Failure when the predicate is false:
Failure(NoSuchElementException("Predicate does not hold for x"))
This is a generic, non-descriptive error that gives the caller no information about which predicate failed or why. If the same if pred else error pattern were made available for Try a more informative failure could be returned, for example:
for {
x <- tryX
if x % 2 == 0 else IllegalArgumentException("x must be even")
} yield x
Background
In for-expressions, if (predicate) is desugared to .withFilter(predicate). withFilter is defined for collections, Option, and Try, but not for Either.
Both Try and Either represent computations that can produce a failure. The key difference is that Try represents computations that may throw exceptions, capturing them as Throwable, while Either[L, R] supports custom error types L.
Therefore, Either cannot support withFilter because a predicate failure would require producing a value of type L, but withFilter has no context-independent way to construct such an L.
Meanwhile,withFilter on Try can only produce a generic Throwable, again because withFilter carries no information about what failure to produce when the predicate returns false.
Proposed improvement
Language change
The key change would be to extend the for comprehension grammar to allow if pred else error and define its desugaring to .withFilterOrElse(pred, error). The existing rule for if without an else would not need to change, making it backwards-compatible.
stdlib change
.withFilterOrElse(pred, error) would need to be defined on Either and Try in the stdlib. This could be implemented as follows for both:
// Either
def withFilterOrElse[A1 >: A](pred: B => Boolean, error: => A1): WithFilterOrElse[A1, B]
// Try
def withFilterOrElse(pred: T => Boolean, error: => Throwable): WithFilterOrElse[T]
In each case, the function returns a WithFilterOrElse object that supports map, flatMap, foreach, and withFilterOrElse, composed with filterOrElse(pred, error) to align with the existing WithFilter implementation and avoid intermediate allocations. For Try, it would also need to support the existing withFilter method.
For Either, filterOrElse already exists in stdlib, so WithFilterOrElse can delegate to it directly. For Try, an equivalent method would need to be added first:
def filterOrElse(pred: T => Boolean, error: => Throwable): Try[T] = this match{
case Success(t) if !pred(t) => Failure(error)
case _ => this
}
While this proposal is limited to stdlib’s Either and Try, the desugaring change would enable any type that defines .withFilterOrElse to use the if-else syntax in for comprehensions. For example, it would be a natural addition to the MonadError typeclass in Cats, or ZIO’s IO type.