cult3

Branching and Conditionals in Elixir

May 30, 2016

Table of contents:

  1. Traditional branching and conditional constructs
  2. Using Multi-clause functions
  3. Conclusion

One of the very first things you typically learn when getting started with programming is branching and conditional logic. This usually involves using if and else, case statements, and while loops.

These early steps of the curriculum are usually very useful for languages such as Ruby, Python, PHP, and Javascript, but in Elixir, there is a better way.

More often than not in Elixir, these constructs can be replaced with multi-clause functions. We looked a multi-clause functions in Multi-clause Functions with Pattern Matching and Guards in Elixir.

If you are coming from another programming language, it might even surprise you when you hear that there are no classical loop constructs in Elixir.

In today’s tutorial we will be looking at branching and conditional logic in Elixir.

Traditional branching and conditional constructs

The really interesting part of Elixir’s branching and conditional functionality is multi-clause functions. However with that being said, there is still a time and place for the more traditional conditional and branching constructs that you will probably be familiar with if you already have experience with another programming language.

Before we look at how we can replace these constructs with multi-clause functions, first we will take a whirlwind tour of what is available in Elixir.

If

The first bit of branching logic that you probably came across as a newbie programmer was the humble if statement. In Elixir this works exactly how you would expect it to work:

if true do
  IO.puts("Hello")
end

If the condition evaluates to true the block of code is run. If this isn’t second nature to you, Elixir is probably not the right language to be learning right now.

You can also have an else block that will be run if the condition does not evaluate to true:

if 0 > 1 do
  IO.puts("0 is greater than 1")
else
  IO.puts("O is not greater than 1")
end

If the code you are writing is really short and concise, you could also write it as a one-liner:

if is_binary("Hello"), do: IO.puts("Binary!"), else: IO.puts("Not binary")

Unless

Elixir also has the unless construct which will run the first branch if the condition evaluates to false:

unless false, do: IO.puts("false"), else: IO.puts("true")

In this example the first block will be executed because the condition is false.

Cond

If you have more than 2 branches that could be chosen, it often makes sense to write the branching logic using the cond construct:

defmodule MyNumber do
  def size(number) do
    cond do
      number <= 3 -> "small"
      number > 3 < 20 -> "medium"
      number >= 20 -> "big"
    end
  end
end

MyNumber.size(23) |> IO.puts()

In this example I’m returning a string based upon the “size” of the number.

If you want to provide a default clause that will be executed if none of the previous conditions were met you can provide a true branch.

defmodule WhatIsIt do
  def check(value) do
    cond do
      is_binary(value) -> "a binary"
      is_number(value) -> "a number"
      true -> "unknown"
    end
  end
end

IO.puts("It's a #{WhatIsIt.check(:atom)}")

In this example I’m providing an Atom (Understanding the Types in Elixir) and so the default statement will be executed.

An important thing to note is, Elixir will check each branch from top to bottom, so if a branch matches it will execute that branch without checking the remaining branches:

defmodule WhatIsIt do
  def check(value) do
    cond do
      true -> "unknown"
      is_binary(value) -> "a binary"
      is_number(value) -> "a number"
    end
  end
end

IO.puts("It's a #{WhatIsIt.check("hello")}")

In this example the default branch will be matched first, and so the more specific is_binary/1 will not be executed.

This is just something to bare in mind when writing these kind of statements.

Case

Finally we have the case expression, which is also our first exposure to pattern matching for branching logic:

defmodule DidItWork do
  def check(atom) do
    case atom do
      :ok -> "it worked!"
      :error -> "it didn't work"
      _ -> "I'm not sure if it worked"
    end
  end
end

IO.puts(DidItWork.check(:ok))
IO.puts(DidItWork.check(:error))
IO.puts(DidItWork.check(:unknown))

In a case construct each branch is pattern matched against the value. If the pattern matches the branch will be executed.

If none of the branches match the pattern we can use the anonymous variable to match anything and therefore provide a default value.

Using Multi-clause functions

If you are already familiar with another programming language, none of the above examples were probably that mind blowing for you. This kind of logic is found in a lot of programming languages and it’s usually one of the very first things you learn.

Whilst you could write this type of code, there is often a better, more declarative way using multi-clause functions.

Here is what the case example looks like using multi-clause functions:

defmodule DidItWork do
  def check(:ok) do
    "it worked"
  end

  def check(:error) do
    "it didn't work"
  end

  def check(_) do
    "I'm not sure if it worked"
  end
end

IO.puts(DidItWork.check(:ok))
IO.puts(DidItWork.check(:error))
IO.puts(DidItWork.check(:unknown))

In this example we’ve rewritten the case construct into three separate functions. When the DidItWork.check/1 function is invoked, the argument will be pattern matched against the function definitions to choose which definition to run.

This is exactly the same as what happened when it was written as a case construct.

However, the benefit of this approach is that each function only deals with one branch of logic. We can use the power of Elixir’s pattern matching to remove the need for an additional construct.

In this example I wanted to show you how the case example could be translated into a multi-clause function example.

But to be honest, this isn’t such a great example because it’s so simple. It’s hard to see the benefit of using multi-clause functions over traditional branching logic.

Lets instead look at a more complicated example.

Imagine we need to get the contents of a webpage from a web service. However, the web service is pretty flakey and so often we won’t get the response we’re looking for.

For illustrative purposes, here is the code to get the response from the web service:

:random.seed(:erlang.timestamp())

defmodule WebService do
  def response do
    Enum.random([
      {:ok, 200, "<p>Hello World</p>"},
      {:error, 500, "Server Error!"}
    ])
  end
end

When we call WebService.response/0 we will get a random tuple from the list that either represents a successful response or an unsuccessful response.

After we have received the response we need to either return the contents of the response or return a message to tell the user to try again later.

First we can decompose the elements of the response tuple:

{status, _, content} = WebService.response()

Next we can use a case to get the contents of the response or return an error message:

response =
  case status do
    :ok -> content
    :error -> "Please try again later"
  end

And finally we can print the response:

IO.puts(response)

Here is the code in full:

:random.seed(:erlang.timestamp())

defmodule WebService do
  def response do
    Enum.random([
      {:ok, 200, "<p>Hello World</p>"},
      {:error, 500, "Server Error!"}
    ])
  end
end

{status, _, content} = WebService.response()

response =
  case status do
    :ok -> content
    :error -> "Please try again later"
  end

IO.puts(response)

This is fine because it works as it’s supposed to, but is there a way we can rewrite it to get rid of the imperative conditional code that is spilling out?

:random.seed(:erlang.timestamp())

defmodule WebService do
  def response do
    Enum.random([
      {:ok, 200, "<p>Hello World</p>"},
      {:error, 500, "Server Error!"}
    ])
  end
end

defmodule ResponseReader do
  def read({:ok, 200, contents}) do
    contents
  end

  def read({:error, 500, _}) do
    "Please try again later"
  end
end

WebService.response()
|> ResponseReader.read()
|> IO.puts()

In this example I’ve encapsulate the functionality of reading the response into a module with two definitions of the read/1 function.

If the response is successful, the first definition will be matched and the contents from the response will be returned.

If the request is not successful we can return the message to try again later.

As you can see, whilst this is still a pretty simple example, it shows how powerful multi-clause functions can be. We can deal with each branch of logic as it’s own single responsibility function.

And the client code does not have to deal with conditional or branching logic depending on the return value of a function. This makes it much easier to write elegant, declarative code that is much nicer to read and understand.

Conclusion

If you have worked with large or complicated codebases, you’ve probably experienced the pain of opening a new file and seeing a mess of branching and conditional logic.

This kind of code is ugly and often leaks important domain logic out into the rest of the code.

Elixir makes it really easy to write beautiful, declarative code using pattern matching and multiclause functions.

I personally really hate imperative branching logic so the realisation that I didn’t have to write it in Elixir was a revelation to me.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.