For else as fold sugar

When reading this post, I started to think about more syntactic feature and I found myself to use fold instead of for-comprehension or after it.

What about using the else keyword for this ? Something like:

for {
 a <- stringNum.toDoubleOption
 if a > 0
} yield log(b) else Double.NaN

as syntactic sugar for

stringNum.toDoubleOption
  .filter(a > 0)
  .fold(Double.NaN)(log(b))

How would this desugar for Seq?

It shouldn’t compile because Seq[A] doesn’t has a method fold[B](=> B, A => B): B: it’s the same name but not the same signature.

The for… else syntax should desugar to one of these signatures:

  • fold(=> B, A => B) like for 1-based monads (Option etc…)
  • fold(A => C, B => C): C like for bi monads like (Either etc…)

But there is another problem: what happens if a class has these two signatures ?

PS: I have to go and I will edit this message later

EDIT: Maybe for... else should only be able to desugar to fold(A => C, B => C): C to avoid conflict. For Options, we could just use a method fold[B](Nothing => B, A => B): B

That seems like it would really limit the applicability of this syntax to a rather small subset of the classes which can be used with the rest of the for comprehension (IIRC: Either, Try, and Option in the standard library).

1 Like

That seems like it would really limit the applicability of this syntax to a rather small subset of the classes which can be used with the rest of the for comprehension

Actually, this change was intented for for comprehension with “common” monads. As you said, Either, Try, Option etc…).

It would also work for 3rd party libs’ monads like Cats’ IO if it had a fold alias for the currently-name redeem (possible via an extension method).

For async like cats, maybe an else inside the for should suit better:

for {
  _ <- putStrLn("How old are you ?")
  n <- readInt
  msg <- s"You are $n years old" else "This is not a valid age!"
  _ <- putStrLn(msg)
} yield ()

It’s a draft but I post it to receive reviews about this idea.

1 Like

I think what doesn’t really make sense to me is that else doesn’t take arguments, so it’s less of a fold and more of a getOrElse, and I’m not really understanding the benefit over just wrapping it:

(for {
 a <- stringNum.toDoubleOption
 if a > 0
} yield log(b)).getOrElse(Double.NaN)

Inside it could be a bit better, particularly for Either/Try/etc as a way to avoid missed guard clauses throwing NoSuchElementExceptions, and while I can see how it might desugar for a specific type, I’m not sure how to make that general (without going full Cats and pulling Applicative and ApplicativeError into the standard library :wink: )

An Either flavored version of your example above:

for {
 a <- stringNum.toDoubleOption.toEither("Must be a double")
 if a > 0 else "Must be positive"
} yield log(a)

might desugar into something like this:

stringNum.toDoubleOption.toEither("Must be a double").flatMap { a => 
  (if (a > 0) Right(a) else Left("Must be positive")).map { a =>
    log(a)
  }
}
6 Likes

Forgot the parameter in my example but I prefer yours with the if/else inside for :ok_hand:

I can see a solution using filterOrElse (already existing in Either)
Taking the last example:

for {
 a <- stringNum.toDoubleOption.toEither("Must be a double")
 if a > 0 else "Must be positive"
} yield log(a)

if a > 0 else "Must be positive" is just a filterOrElse.
This should just desugar to:

stringNum.toDoubleOption.toEither("Must be a double")
  .filterOrElse(a => a > 0, a => "Must be positive")
  .map { a =>
    log(a)
  }

Every monad having a filterOrElse(B => Boolean, => A) can use this syntax.

If I use a library that doesn’t support this, I can add this method using an extension:

extension[A] (io: IO[A]) {

  def filterOrElse(cond: A => Boolean, zero: => A): IO[A] = io.map { a =>
    if(cond(a)) a else zero
  }
}

Note: As you can see, I use the same generic type (A) for cond and zero. It means I can do

for {
  a <- foo //type A
  if condition(a) else b //b should have the type A
}

Al contrario of an Either which have the method filterOrElse[A1 >: A](p: (B) => Boolean, zero: => A1): Either[A1, B]. We could also use a union to allow different types.

1 Like

That looks like a good improvement to if (which currently isn’t terribly useful for Either & similar).

I’m not sure if it’s a good thing or a bad thing that this would result in two versions of if that are not isomorphic. However the current behavior of if is highly variable depending on the Monad in question, so that might not be a bad thing as this variant would be much more regular.

For whatever it’s worth, I like it :+1:

1 Like