7

In Scala, I have progressively lost my Java/C habit of thinking in a control-flow oriented way, and got used to go ahead and get the object I'm interested in first, and then usually apply something like a match or a map() or foreach() for collections. I like it a lot, since it now feels like a more natural and more to-the-point way of structuring my code.

Little by little, I've wished I could program the same way for conditions; i.e., obtain a Boolean value first, and then match it to do various things. A full-blown match, however, does seem a bit overkill for this task.

Compare:

obj.isSomethingValid match {
  case true => doX
  case false => doY
}

vs. what I would write with style closer to Java:

if (obj.isSomethingValid)
  doX
else
  doY

Then I remembered Smalltalk's ifTrue: and ifFalse: messages (and variants thereof). Would it be possible to write something like this in Scala?

obj.isSomethingValid ifTrue doX else doY

with variants:

val v = obj.isSomethingValid ifTrue someVal else someOtherVal

// with side effects
obj.isSomethingValid ifFalse {
  numInvalid += 1
  println("not valid")
}

Furthermore, could this style be made available to simple, two-state types like Option? I know the more idiomatic way to use Option is to treat it as a collection and call filter(), map(), exists() on it, but often, at the end, I find that I want to perform some doX if it is defined, and some doY if it isn't. Something like:

val ok = resultOpt ifSome { result =>
  println("Obtained: " + result)
  updateUIWith(result) // returns Boolean
} else {
  numInvalid += 1
  println("missing end result")
  false
}

To me, this (still?) looks better than a full-blown match.

I am providing a base implementation I came up with; general comments on this style/technique and/or better implementations are welcome!

Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
  • 7
    What exactly is wrong with an `if` statement? You can either use this simple, familiar control structure that is used extensively everywhere in every language, or you can create a gimmicky abstraction that is a mental speedbump for any new developer and doesn't provide any advantage over `if` in the first place. – ryeguy Apr 13 '11 at 18:50
  • There's `if` for Booleans, but what about simple `match`es for `Option`? – Jean-Philippe Pellet Apr 13 '11 at 19:00
  • For `Option`, there is `getOrElse`. – Madoc Apr 15 '11 at 06:23
  • So, a different way to treat each situation. A more consistent treatment with ifTrue/ifFalse and ifSome/ifNone thus sounds good to me… – Jean-Philippe Pellet Apr 15 '11 at 07:36
  • See also this discussion on the mailing list: http://www.scala-lang.org/node/3496 – Jean-Philippe Pellet May 26 '11 at 07:45

3 Answers3

14

First: we probably cannot reuse else, as it is a keyword, and using the backticks to force it to be seen as an identifier is rather ugly, so I'll use otherwise instead.

Here's an implementation attempt. First, use the pimp-my-library pattern to add ifTrue and ifFalse to Boolean. They are parametrized on the return type R and accept a single by-name parameter, which should be evaluated if the specified condition is realized. But in doing so, we must allow for an otherwise call. So we return a new object called Otherwise0 (why 0 is explained later), which stores a possible intermediate result as a Option[R]. It is defined if the current condition (ifTrue or ifFalse) is realized, and is empty otherwise.

class BooleanWrapper(b: Boolean) {
  def ifTrue[R](f: => R) = new Otherwise0[R](if (b) Some(f) else None)
  def ifFalse[R](f: => R) = new Otherwise0[R](if (b) None else Some(f))
}
implicit def extendBoolean(b: Boolean): BooleanWrapper = new BooleanWrapper(b)

For now, this works and lets me write

someTest ifTrue {
  println("OK")
}

But, without the following otherwise clause, it cannot return a value of type R, of course. So here's the definition of Otherwise0:

class Otherwise0[R](intermediateResult: Option[R]) {
  def otherwise[S >: R](f: => S) = intermediateResult.getOrElse(f)
  def apply[S >: R](f: => S) = otherwise(f)
}

It evaluates its passed named argument if and only if the intermediate result it got from the preceding ifTrue or ifFalse is undefined, which is exactly what is wanted. The type parametrization [S >: R] has the effect that S is inferred to be the most specific common supertype of the actual type of the named parameters, such that for instance, r in this snippet has an inferred type Fruit:

class Fruit
class Apple extends Fruit
class Orange extends Fruit

val r = someTest ifTrue {
  new Apple
} otherwise {
  new Orange
}

The apply() alias even allows you to skip the otherwise method name altogether for short chunks of code:

someTest.ifTrue(10).otherwise(3)
// equivalently:
someTest.ifTrue(10)(3)

Finally, here's the corresponding pimp for Option:

class OptionExt[A](option: Option[A]) {
  def ifNone[R](f: => R) = new Otherwise1(option match {
    case None => Some(f)
    case Some(_) => None
  }, option.get)
  def ifSome[R](f: A => R) = new Otherwise0(option match {
    case Some(value) => Some(f(value))
    case None => None
  })
}

implicit def extendOption[A](opt: Option[A]): OptionExt[A] = new OptionExt[A](opt)

class Otherwise1[R, A1](intermediateResult: Option[R], arg1: => A1) {
  def otherwise[S >: R](f: A1 => S) = intermediateResult.getOrElse(f(arg1))
  def apply[S >: R](f: A1 => S) = otherwise(f)
}

Note that we now also need Otherwise1 so that we can conveniently passed the unwrapped value not only to the ifSome function argument, but also to the function argument of an otherwise following an ifNone.

Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
6

You may be looking at the problem too specifically. You would probably be better off with the pipe operator:

class Piping[A](a: A) { def |>[B](f: A => B) = f(a) }
implicit def pipe_everything[A](a: A) = new Piping(a)

Now you can

("fish".length > 5) |> (if (_) println("Hi") else println("Ho"))

which, admittedly, is not quite as elegant as what you're trying to achieve, but it has the great advantage of being amazingly versatile--any time you want to put an argument first (not just with booleans), you can use it.

Also, you already can use options the way you want:

Option("fish").filter(_.length > 5).
  map (_ => println("Hi")).
  getOrElse(println("Ho"))

Just because these things could take a return value doesn't mean you have to avoid them. It does take a little getting used to the syntax; this may be a valid reason to create your own implicits. But the core functionality is there. (If you do create your own, consider fold[B](f: A => B)(g: => B) instead; once you're used to it the lack of the intervening keyword is actually rather nice.)


Edit: Although the |> notation for pipe is somewhat standard, I actually prefer use as the method name, because then def reuse[B,C](f: A => B)(g: (A,B) => C) = g(a,f(a)) seems more natural.

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • Thanks for the explanations! Indeed, this pipe operator is much more general than my proposal. I find the lack of intermediate keyword causes a diminished readability, especially when two blocks follow each other… I can easily add an apply() method to my OtherwiseX objects and forward it to otherwise, or the other way round. As you wrote: getting used to the native syntax of Option takes time… I still find it rather cryptic at times, if nested multiple times (plus, I have to check each time if it's actually a collection or an Option), whereas ifX and otherwise seem clearer to me. – Jean-Philippe Pellet Apr 13 '11 at 19:39
2

Why don't just use it like this:

val idiomaticVariable = if (condition) { 
    firstExpression
  } else { 
    secondExpression 
  } 

?

IMO, its very idiomatic! :)

tuxSlayer
  • 2,804
  • 2
  • 20
  • 24
  • Well, like ryeguy's comment, this works with if(boolean), but what about Option, then? – Jean-Philippe Pellet Apr 14 '11 at 09:17
  • Personally I see no difference between `value match { case A => doA; case B => doB() }` with only two variants and `if (cond) doA else doB;` statement. Morevover, `if` looks much more familiar to many programmers and it expresses intent: a kind of binary decision. Thus, its ok to write `if (opt.isDefined) doA else doB` in case you have some actions not related to `opt` and can't `opt map` or `opt flatMap` to these actions. To summarize, see no problem here... – tuxSlayer Apr 15 '11 at 14:33