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

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.

1 Like

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)

Better suggestion? :smiley:

ifLet or ifMatch ?

PartialFunction.condOpt(x){ case Foo(f) if p(f) => bar(f) }

is equivalent to the much better-known

Some(x).collect{ case Foo(f) if p(f) => bar(f) }

so it’s not much of a surprise that it’s little-known. condOpt may have a modest performance advantage, but it would usually be surpassed by

x match {
  case Foo(f) if p(f) => Some(bar(f))
  case _ => None
}

And if you already have the partial function, then you can use either of

pf.lift(x)
pf.unapply(x)

(the latter existing only in 2.13+).