Could we have Ruby's "unless" in Scala3?

for ergonomics, it’s such a small simple thing yet it makes reading most of what would otherwise be if statements with negated conditions so much nicer. It is currently a thing in Ruby, just to refer to some prior art.

Maybe the pattern matching guards syntax could also allow for this just to keep being similar to the choice of if/unless for simple conditionals?

unless 1 to 8 contains rank
  doSomething()
else
  thingsAreOK()

The proposed unless is nice, but I’m pretty sure the price of an extra keyword is too much for most.

If you can somehow make this work with existing keywords it may fly. But then, what’s wrong with using negation.

If there is any control structure I miss, it is the repeat/until loop. You know, the loop that gets executed at least once. Like do/while in C/C++, but with inverted condition. If you need to execute a loop at least once and all you have is a while loop, it feels unnecessarily awkward to implement.

Nothing, this obviously doesn’t add anything compared to negated if conditions other than ergonomics/readability.

You could have

unless 1 to 8 contains rank

but without unless you have to write

if ! (1 to 8 contains rank)

which is somewhat noisier and for most probably requires a bit more effort to parse.

Can you propose something without adding a new keyword?

How about:

if false 1 to 8 contains rank

I don’t care for it personally, and IMO it doesn’t carry its weight to add to the language per se. But if you were willing to compromise slightly (parens around the predicate clause, and probably use some name other than the keyword else), I suspect that you could implement this yourself in userland code…

1 Like

Please also count me among those who have tired of negation.

Booleans are already hard enough to work with, but negation doubles my chances of getting it wrong.

I definitely endorse your proposal, or at least I will wear the t-shirt if it says: “unless is more!”

Having to wrap expressions in extra parens is also intolerable.

We have some small relief in b.pipe(!_):

if p & q pipe (!_) then 42 else 27

I wonder if it is too late to SIP swapping & and && in Scala 3.

5 Likes

What about if not?

3 Likes

Scala has seven control expressions: if, match, while, for, try, throw, return. Any proposed addition has to be at least as important as these ones - and preferably as important as the first five, since keeping throw and return would be up for debate if it wasn;t for historical reasons.

So far, I have seen nothing that comes close to that standard.

5 Likes

if , match , while , for , try , throw , return

There’s also do-while!

1 Like

Not anymore :wink:

3 Likes

What happened to yield?

1 Like

for (x <- xs unless x < 0) yield x

In case anyone else wondered why “unless” instead of “unif” (as in un-if), the dictionary says it’s shortened from “not on less”.

By that logic, the methods on Future should be onSuccess and unSuccess. I guess that’s why they deprecated them.

Along similar lines, except is sad it wasn’t picked for team Scala. It could be used for match inversion and to replace catch. So the bar should be that the keyword is used in at least two expressions with totally different semantics. Obviously underscore passes that test, but while must be deprecated in favor of tailrec.

One way to save while is to make it a built-in Duration. Instead of tweaking tests to run for some reasonable duration before failing under Travis, a test could wait for a while, defined to be a reasonable length of time under the circumstances. Viktor could ask on concurrency-interest for JDK support of the while. Should I post that idea on the thread about how to make Scala popular again?

1 Like

Pascal has repeat <action> until <condition> syntax which reverses the condition passed to do <action> while <condition>.

As a non native English speaker I can say that “unless” sounds somewhat more confusing than “if not” because I hear “unless” much less frequently. OTOH double negation is even more confusing, especially with complex conditions, but that’s not that frequent.

The best solution would be to have mandatory braces around if arms, but no parentheses around condition, so instead of:

unless 1 to 8 contains rank
  doSomething()
else
  thingsAreOK()

There would be:

if !(1 to 8 contains rank) {
  doSomething()
} else {
  thingsAreOK()
}

Clear enough, although in this example I would think about swapping if arms to make things even clearer.

3 Likes

As a native English speaker, I’m fine using the word “unless” in speech, but I find it just plain hard to read correctly in code. I know what it means intellectually, but my eyes reflexively invert the positive and negative arms…

I have a number of methods in my utilities.

/** onlyIf-do. Only if the condition is true, perform the effect. */
  @inline def oif[U](b: Boolean, vTrue: => Unit): Unit = if(b) vTrue

/** if-else. If the condition is true, use 2nd parameter else use 3rd parameter. */
@inline def ife[A](b: Boolean, vTrue: => A, vFalse: => A): A =
 if (b) vTrue else vFalse

/** ifNot-else. If the condition is false, use 2nd parameter else use 3rd parameter. */
@inline def ifne[A](b: Boolean, vNotTrue: => A, visTrue: => A): A =
 if (b) vNotTrue else vNotTrue

/** if-elseif-else. If the first condition is true, use 2nd parameter else if the second condition in parameter 3 is true use 4th parameter. */
@inline def ife2[A](b1: Boolean, vTrue1: => A, b2: => Boolean, vTrue2: => A, vElse: => A): A =
 if (b1) vTrue1 else if (b2) vTrue2 else vElse

I would normally use:

ife((1 to 8).contains(rank), thingsAreOK(), doSomething())

but would use

ifne((1 to 8).contains(rank), doSomething(), thingsAreOK())

if I felt the latter better conveyed meaning.

1 Like

While we’re throwing around fun control structures, I’ve wondered why some sort of if-let is not more popular in Scala. Basically, a match where you don’t have to be total

  private[this] val sentinel = new AnyRef {}
  private[this] val sentinelF = (_: Any) => sentinel

  def ifLet[A, B](value: A)(pf: PartialFunction[A, B]): Option[B] = {
    val result = pf.applyOrElse(value, sentinelF)
    if (result.asInstanceOf[AnyRef] eq sentinel) None else Some(result).asInstanceOf[Option[B]]
  }

  
  // example usage
  val secondElement: Option[Int] = ifLet(List(1,2,3)) {
    case _ :: x :: _ => x
  }
 
  // or also useful for side effecting

  val result: Either[Throwable, Int] = ???
  ifLet(result) {
    case Left(t) => println(t.getMessage)
  }

I find that really hard to parse.

2 Likes

Not sure, internally we have something like this for map. Two versions, come to think of it, which vary based on what happens if the match fails: one leaves the argument unchanged, the other falls back to a default handler.

The optional braces work really well when the statements are small, so I think having them be mandatory would be a step backwards for readability.

FWIW: most of the time I end up negating a conditional, I do it so I can setup the order of the if arms to make things easier to read. The general form looks something like this:

if (!validation)
  oneLineErrorResult
else {
  considerably
    .longer
    .happy
    .path
    .block
}

Inverting it would make the conditional easier to read, at the cost of moving the bit that produces the error result further away from the conditional that produced it. This makes a huge difference when you end up chaining them because you have a series of simple validation checks that don’t necessarily warrant being pulled out into monadic helpers:

if (!validation0) oneLineErrorResult0
else if (!validation1) oneLineErrorResult1
else if (!validation2) oneLineErrorResult2
else if (!validation3) oneLineErrorResult3
else if (!validation4) oneLineErrorResult4
else if (!validation5) oneLineErrorResult5
else {
  considerably
    .longer
    .happy
    .path
    .block
} 

It’d be nice to be able to write something like this:

unless (validation0) oneLineErrorResult0
else unless (validation1) oneLineErrorResult1
else unless (validation2) oneLineErrorResult2
else unless (validation3) oneLineErrorResult3
else unless (validation4) oneLineErrorResult4
else unless (validation5) oneLineErrorResult5
else {
  considerably
    .longer
    .happy
    .path
    .block
} 
3 Likes

I wouldn’t mind dropping return in order to make room for unless (or dropping it just for the sake of dropping it) but I guess if disregarding the historical reasons would be on the table it would’ve already happened.

1 Like