typelevel/mouse

whenA and unlessA do not have lazy functions

Closed this issue · 6 comments

Is there a reason the Boolean functions whenA and unlessA are not by-name (lazy) functions like option, xor, fold etc?
Now when using false.whenA(f) function f is evaluated even when the boolean is false. Similar for unlessA.
Is this a bug or a feature?

Functions like Option.when and Option.unless are also by-name, so I would have expected these Boolean function to also be by-name.

Honestly, I don't know why whenA and unlessA have call-by-value semantic for F[A] in mouse for sure, I can only make a hypothesis. Underlying methods in the Applicative have call-by-name semantic (even in Scalaz https://github.com/scalaz/scalaz/blob/ea81ca782a634d4cd93c56529c082567a207c9f6/core/src/main/scala/scalaz/syntax/std/BooleanOps.scala#L218).
However, these methods on Applicative syntax have call-by-value semantic for F[A] see typelevel/cats#3899.
So that could be the reason for the same behavior in the mouse.

Applicative[List].whenA(false)(List(println("Direct method"))) // will print nothing

List(println("Syntax method")).whenA(false) // will print

false.whenA(List(println("Syntax method"))) // will print

And I am convinced we can't change that semantic due to bincompat guarantees.
Probably you can use fold to have call-by-name semantic.

I think this is an unfortunate oversight (aka "bug"). My bad for not picking up at PR time.

So, how do we move forward, accepting the constraint that we can't break existing callers of whenA etc?

Im open to

  • Add lazy variants of whenA/unlessA. What to call them? Do we have any precedents in Typelevel ecosystem to follow?
  • Put some policy in DEV.md clarifying that operators that take conditionally executed code blocks should generally by by-name.

We could add lazy variants for these methods, I agree. Probably whenA_ and unlessA_ for names is good? Anyway, I have no other suggestions.

In the cats issue, Oscar mentions that the underscore suffix has a different implication.

Another alternative is L for Lazy; whenAL & unlessAL. Its short and the reason for the L is at least sane, although not immediately self-evident

Also, we could have different names for those methods - ifTrueF and ifFalseF:

val boolean: Boolean = ???
def computation[F[_]: Applicative]: IO[Unit] =  Applicative[F].unit

val result1: IO[Unit] = boolean.ifTrueF(computation[IO]) // same as `whenA` but lazy one
val result2: IO[Unit]  = boolean.ifFalseF(computation[IO]) // same as `unlessA` but lazy one

What do you think @benhutchison @diversit?

I would find it confusing that there's two method pairs that have completely different names but do almost the same thing. Not making them by-name was a mistake originally that we need to recover from as gracefully as possible. Unfortunately it claimed the best method name, but I'd still like a variant on that.
I didnt get any feedback to my comment on cats issue, so let's make a local decision.

Like whenA and unlessA but lazy in the args. Options:

whenAL and unlessAL: L means lazy. clear what L suffix means once you know it, but not before.

whenA_ and unlessA_: _ means 'variant' I guess. Visually nicer. Overloads _ to mean different things as notes in the cats issue, not a clear principle. But maybe thats OK, its a generic suffix that indicates a variant method.

L is my pref suffix, but I'd happily meet you on a _ suffix (over a different name) so we can move forwards..?