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

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.

1 Like

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

It’d be nice to be able using else for optional code block:

{doSomething1()} else {doSomething2()}
//instead of 
({doSomething1()}, {doSomething2()})

If it were possible it would be very easy to implement custom conditional operators.

  def unless(cond: =>Boolean)(b1: =>Unit):Unit = ??? 
  def unless(cond: =>Boolean)(b1: =>Unit, b2: =>Unit):Unit = ??? 

unless (validation0) {oneLineErrorResult0}
else unless (validation1) {oneLineErrorResult1}
else unless (validation5) {oneLineErrorResult5}
else {
  considerably
    .longer
    .happy
    .path
    .block
} 
2 Likes

I’m reasonably in favor of unless from the standpoint of avoiding logical errors in writing and manipulating code. To me, representing business logic is core to what product engineers do.

But maybe the bigger question is whether Scala lacks a reasonable way to implement a zero-cost unless construct in code. I’m guessing the missing features would be second-class functions (for the statement blocks) and less constrained syntax definitions.

Obviously there are some risks of everyone’s Scala looking different, depending on what constructs they might want to adopt, as can be the case in Ruby. But I can also see upsides in less hackery to make DSLs.

Not sure what you mean by second-class functions, but I think an unless implemented in Scala can look reasonably first-class – indistinguishable from native if if you normally use braces with if anyway:

object unlessSyntax {
  @inline def unless[A](b: Boolean)(t: => A): Unless[false, A] = {
    if (b) Unless(t)
    else null.asInstanceOf[Unless[false, A]]
  }

  private[unlessSyntax] type Unless[Inhabited <: Boolean, +A] <: AnyRef
  @inline private[this] def Unless[Inhabited <: Boolean, A](a: A): Unless[Inhabited, A] = a.asInstanceOf[Unless[Inhabited, A]]

  implicit final class UnlessSyntax[Inhabited <: Boolean, +A](private val self: Unless[Inhabited, A]) extends AnyVal {
    @inline def `else`[B >: A](f: => B): Unless[true, B] = {
      if (self eq null) Unless(f) else self.asInstanceOf[Unless[true, A]]
    }
  }

  @inline implicit def unpack[A](unless: Unless[true, A]): A = unless.asInstanceOf[A]
  @inline implicit def unpackUnit(unless: Unless[false, _]): Unit    = ()
}

import unlessSyntax.unless

//val x: Int = // as expected, one-sided unless cannot be assigned to Int
val x: Unit = unless(1 == 3) {
  println("no!")
  1
}

// this is fine
val i = unless(true) {
  8
} `else` {
  4
}

// no boxing:
println(i)
// 8
println {
  unless(i / 2 == 1) {
    s"xa $i"
  } `else` s"xb $i"
}
// xb 8

This is completely free allocation-wise. Should also be free method call-wise if Scalac inliner is enabled or if unless is expanded by a macro.

2 Likes

My previous comment about the word unless got it wrong: it should be pronounced like shapeless. What we’re doing without are negatory prefixes like un or !.

unless should do for boolean syntax what shapeless does for arity.

1 Like

This is already in the standard library: PartialFunction.condOpt.

But it isn’t “popular” in the sense that I think most Scala users don’t even know it is there.

4 Likes

Hey that’s great! Maybe its unpopularity has something to do with obscure name and import path :slight_smile:

2 Likes

I believe to match Rust if let (which is really nice, although impure by design) it should return Unit, and not Option (as condOpt) or Boolean (as cond).

Something like:

def ifLet[T](value: T)(handler: PartialFunction[T, Unit]): Unit = {
  handler.applyOrElse[T, Unit](value, _ => ())
}

val x: Option[Int] = Some(5)

ifLet(x) {
  case Some(value) => println(value)
}

(though this is getting off topic both in language and in construct)