Erlang is a fantastic programming language, but its flow is sometimes hard to think about if you spend a lot of time in the C family of languages. In a C-like language, it often makes sense to use “if” statements and early returns to check conditions. Here’s a Dart snippet that would probably compile in half a dozen languages with minor changes:

/// Checks to see if we are connected and then send a message from the queue. Return the remaining queue.
/// If not connected, reconnect and try again. Otherwise, return the empty queue back.
List processQueue(List messageQueue) {
  if (this.connected == false) {
    socket = connect();
    return processQueue(messageQueue);
  }
  if (messageQueue.isEmpty) {
    return messageQueue;
  }

  socket.send(messageQueue.first);

  return messageQueue.sublist(1, messageQueue.length);
}

 

Erlang and Nested Case Statements

One of the things that often makes Erlang code ugly is nested case statements because there really aren’t ‘if’ statements in the way we typically think of them. So, a first crack at implementing this same function in Erlang would yield this monstrosity:

loop(State) ->
  case State#state.connected of
    true ->
      case State#state.queue of
        [] ->
          State;
        [Msg | Remaining] ->
          send(State#state.socket, Msg),
          State#state{queue = Remaining}
      end;
    false ->
      case reconnect(State#state.connection_info) of
        {true, ConnectedState} ->
          loop(ConnectedState);
        {false, _Error} ->
          State
      end
  end.

This is a nested case statement, and nested case statements are really tough to read. Perhaps a bit of hyperbole, but the most important thing to me about code is its readability. Yes, even more important than whether it works or not: because if it doesn’t work, at least someone else can fix it because they can read it!

Erlang function clauses are the first line of defense against nested case statements. You can separate the possible cases of the first conditional into different function clauses:

loop(#state{connected = true} = State) ->
  case State#state.queue of
    [] ->
      State;
    [Msg | Remaining] ->
      send(Msg),
      State#state{queue = Remaining}
  end;
loop(#state{connected = false} = State) ->
  case reconnect(State#state.connection_info) of
    {true, ConnectedState} ->
      loop(ConnectedState);
    {false, _Error} ->
      State
  end.

In most situations, you can continue to remove all case statements by breaking each case into its own function:

process_queue(Socket, []) ->
  [];
process_queue(Socket, [Msg | Remaining]) ->
  send(Socket, Msg),
  Remaining.

loop(#state{connected = true} = State) ->
  NewQueue = process_queue(State#state.socket, State#state.queue),
  State#state {
    queue = NewQueue
  };
loop(#state{connected = false} = State) ->
  ...

This can be good or bad. Sometimes Erlang code becomes hard to read because it jumps around a lot. There’s a fine line between short, readable functions and being a human-call stack cyborg. There isn’t a one-size-fits-all solution.

But let’s say we go with the case statement, what happens if there is another condition to sending a message? For example, we have a flag that prevents sending messages temporarily in State. Now we’re back to nested case statements:

loop(#state{connected = true} = State) ->
  case State#state.prevent_send of
    true ->
      State;
    false ->
      case State#state.queue of
        [] ->
          State;
        [Msg | Remaining] ->
          send(State#state.socket, Msg),
          State#state{queue = Remaining}
      end;
  end;
loop(#state{connected = false} = State) ->
  ...

Another tool to clean this up is to put the conditions in a tuple and evaluate them in a single case statement:

loop(#state{connected = true} = State) ->
  case {State#state.prevent_send, State#state.queue} of
    {true, _} ->
      State;
    {false, []} ->
      State;
    {false, [Msg | Remaining]} ->
      send(State#state.socket, Msg),
      State#state{queue = Remaining}
  end;
loop(#state{connected = false} = State) ->
  ...

We can achieve the same thing by passing the values in the tuple as arguments to another function and have multiple clauses if we’re cool with jumping around a bit:

process_queue(_, Queue, true) ->
  Queue;
process_queue(_, [], _) ->
  [];
process_queue(Socket, [Msg | Remaining], false) ->
  send(Socket, Msg),
  Remaining.

loop(#state{connected = true} = State) ->
  NewQueue = process_queue(State#state.socket, State#state.queue, State#state.prevent_send),
  State#state {
    queue = NewQueue
  };
loop(#state{connected = false} = State) ->
  ...

In this particular case, I’d opt for not jumping around because I can’t figure out what ‘true’ or ‘false’ means in the context of process_queue. But then again, this is just a contrived example.

You’ve seen several common ways people write code like this in Erlang. Some of it is easier to read than others, and what style you may find easier to read for your project could vary. Just be aware that other humans will be the primary consumers of your code, and take care when structuring it to be easy to understand by those people who will be reading your code.

Joe Conway

Founder at Stable Kernel

Leave a Reply

Your email address will not be published. Required fields are marked *