Erlang from the ground up

Decisions: case and if

The two decisions inside a function body: case, which matches a value against several patterns, and if, which is a ladder of guards with a required final true.

Part 5 of 12

Intermediate17 min readBack to Courses

case matches a value

case takes one value and compares it against a list of patterns, separated by semicolons, until one matches. Each branch has a body, and the whole case expression gives back whatever the chosen body gives back. It is the same clause selection as a function head, applied to a value you already have.

A case needs an end, and every branch ends with a semicolon except the last. If no pattern matches, the run stops with a case clause error, so a final branch of underscore is a common way to say what happens in every other situation.

Because case is an expression, it can sit inside a larger expression: on the right of a match, inside a function call, or as the body of another function. It gives a value, not a jump.

if is a ladder of guards

if has no subject at all. Each branch is a guard followed by an arrow, and Erlang picks the first guard that is true. There is no pattern to match, so the branches ask questions about values already in hand.

The one rule that catches everyone: if requires a final true -> branch. If every guard is false and there is no true clause, the expression fails with an if clause error. Writing the last branch as true -> is the Erlang way of saying otherwise.

Guards in an if must produce the atom true to be taken. X > 3 does, but a comparison that is false simply moves on; there is no notion of a truthy value in Erlang, so 1 or a non-empty list would never be accepted as a condition.

Choosing between them

Reach for case when the shape of a value decides what happens: a tagged tuple, a list, a small set of atoms. Reach for if when the decision is a set of numeric bands and there is no value to take apart. Both can appear in the same function, and neither needs a new function of its own.

One more difference is where the test lives. A function head can carry the same pattern and guard as a case branch, so a two-clause function often replaces a case entirely. When the decision is the only thing the function does, the clauses are shorter; when the value has already been computed and is used again in the body, the case keeps it in one place.

A case, then an if

-module(prog).
-export([main/1]).

main(_) ->
    io:format("~p~n", [label(0)]),
    io:format("~p~n", [label(3)]),
    io:format("~p~n", [size_of(5)]),
    io:format("~p~n", [size_of(50)]).

label(N) ->
    case N of
        0 -> none;
        M when M > 2 -> many;
        _ -> a_few
    end.

size_of(N) ->
    if
        N > 10 -> big;
        N > 3 -> medium;
        true -> small
    end.

Two ways to decide

Run it and follow which branch each call takes. Then move the `M when M > 2` branch below the underscore branch and explain the new output.

Output
Click "Run" to execute your code

Leave out the true -> branch of an if and the run fails with an if clause error as soon as every guard is false. It is not a warning and not a default; the expression simply has no answer.

A case gives back the value of the branch

case is an expression, so the value of the chosen branch is the value of the whole thing. That is why a case can sit on the right of a match, inside a function call, or as the body of a function. The branches are expressions too, so each one has to produce a value the surrounding code can use.

The branches do not have to return the same type, but they usually should. A case that answers an atom on one path and a tuple on another pushes the difference onto every caller, which then needs a second case to find out what it received. A consistent shape is easier to consume.

if is a ladder with no subject

if has no value to match. Each branch is a guard, and the first guard that returns the atom true wins. A guard that returns false is skipped, and a guard that returns anything else is not true at all, so Erlang moves on. There is no truthy one and no truthy non-empty list.

That is the whole difference between case and if. Use case when the shape of a value decides, and if when a set of comparisons does. Both are expressions, both need an end, and both fail loudly when nothing matches.

Three mistakes dominate this chapter. A branch body ends with a comma, the next branch starts after a semicolon, and the whole expression ends with end; getting those three separators wrong is a syntax error. An if with no final true -> fails when every guard is false. And if 1 -> never runs, because 1 is not the atom true.

Try these next

  • Add a case branch for negative numbers and call it with -1.
  • Rewrite the size_of function with case and guards, then compare the two versions.
  • Give an if a guard that returns a number instead of a boolean and explain the error.

Shipping cost from a basket

Run it and check each answer against the count and weight. Then add a basket with 0 items and a heavy weight, and decide what it should cost.

Output
Click "Run" to execute your code

Check what you learned

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

Part 1 of 5

Why does an if usually end with true ->?