Scala from the ground up

Decisions: if and match

if as an expression that hands back a value, match as a decision table over numbers and strings, guards, and the underscore that catches everything left.

Part 4 of 12

Beginner14 min readBack to Courses

A choice that produces a value

if in Scala is an expression: it evaluates to whatever branch it takes, so it can stand on the right of a val and the name then holds the answer. The condition goes in round brackets, and there is no separate statement form to learn — the same shape works whether you are choosing a value or choosing what to print.

The condition has to be a Boolean: a comparison such as score >= 60, or two conditions joined with && and ||. There is no truthiness here. A number is not a stand-in for true, and a String is not a stand-in for false, so the compiler stops you before that habit starts.

match, which is a switch with a value

match takes a value and tries the cases in order, from the top down, until one of them fits. case 1 => ... compares against a literal, case 2 | 3 => ... names two literals on one line, and case _ => ... catches whatever is left. Each arrow is followed by the result for that case, so a match is an expression too.

Matching on text is the everyday case in real code: a command word, a status, a day name. Matching on numbers is just as direct. The habit worth forming is the underscore at the end — leave it out and the compiler complains whenever a value could slip past every case, which is the language asking you to say what anything else means.

Guards, and the else you have to write

A guard is an if written after the pattern: case x if x < 0 => "negative". That case fits only when the guard is also true, so the same value can be tested several times with different conditions. This is how a range is expressed without any clever arithmetic in the pattern itself.

An if used for its value needs a branch for both outcomes: if (n > 0) "up" else "down". Leave the else off and the expression becomes Unit, which means it printed something rather than produced one. Read a crowded if chain as a decision table — the first true line wins, and the order of the lines is the logic.

How a match is read

A match is not a set of conditions tested at once. It reads from the top down and stops at the first case that fits, so the order of the cases is part of the logic. Put the narrow cases first: a case _ above a guarded case makes the guarded one unreachable, and the compiler may warn that the later line can never run.

Every case has to produce a value of the same kind, because the whole match is one expression. If no case fits, the program throws a MatchError at that moment; the underscore is what turns the table into one that cannot fall through. For a number, the compiler may only warn about the gap, so read that warning.

An if is also an expression, and its two branches have to agree in type. Leave the else off and the missing branch becomes Unit, the type of a piece of work that produces nothing to keep. Conditions short-circuit: the right side of && is only evaluated when the left side is true, and the right side of || only when the left side is false.

A condition is checked, not converted

A condition must be a Boolean, so a name that might be empty is tested with name.isEmpty or name != "" rather than with name alone. The compiler rejects the shortcut, which is the point: an empty string and a missing value are different questions, and the language makes you pick one.

Three habits cause trouble here: writing = where a comparison needs ==, putting a general case above a specific one, and using an if without else when you meant to choose a value. The first is a compile error; the second leaves an unreachable case, and the third gives you Unit on the false branch.

Try these next

  • Write a match that names a traffic light colour, with a fallback for anything else.
  • Turn a score into a letter with an if chain, then rewrite the same chain as a match with guards.
  • Move a case _ to the top of a match and read the compiler warning.

Choosing a band, then naming a value

val score = 82

// if hands back a value.
val pass = score >= 60
val band = if (score >= 90) "gold" else "silver"

// match over a number, with a fallback.
val label = 3 match {
  case 1 => "small"
  case 2 | 3 => "medium"
  case _ => "large"
}

// match over text.
val kind = "sat" match {
  case "sat" | "sun" => "weekend"
  case _ => "weekday"
}

// A guard tests more than the pattern.
val n = -7
val shape = n match {
  case x if x < 0 => "negative"
  case x if x == 0 => "zero"
  case _ => "positive"
}

Grading a score

Run it and check each answer against the value above it. Then change `score` to 95 and `n` to 0, and read the output again without touching the rest.

Output
Click "Run" to execute your code

Postage for a parcel

Run it and check each price against the weight. Then set `express` to true, change the weight to 300, and follow the same lines again.

Output
Click "Run" to execute your code

A match has to answer for every value it can be given. Without a case _ at the end, a number you did not list, or a string you did not name, makes the compiler stop with a warning or an error.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What type does if (score >= 60) "pass" else "fail" have?