Elixir pattern matching - save your time, similar with the way of our thinking
Author: Manh Vu
Published: 2024-07-08

Intro

Once of interesting features of Elixir is pattern matching. That is way to save your time and similar with the way of our thinking.

Pattern matching help us to check value, extract value from complex term, branching code.

It also is a way to fail fast, follow “Let it crash” (and supervisor will fix) idiomatic of Erlang.

How to uses

Pattern matching is always return original term (value of variable/expression) for us can continue using it for other expression.

Three things we need to care in here:

  • If does not match it will raise an error.
  • Pattern matching is always return original term.
  • If we add a variable for extracting, after matched variable will have value we expected.

With ‘=’, pattern is in the left side, original term in right side.

We can use many pattern in an expression like:

{:ok, result} = {:ok, {total, list}} = {:ok, {1, ["hi"]}}.

With function, pattern is placed in argument.

No we go to two use cases for using pattern matching.

check similar(condition) value

This type of pattern matching can help us verify a value, format of variable/param.

Term is matched with pattern ({:ok, “hello”} = {:ok, “hello”}): verify value (check value and return original term)

Term is unmatched with pattern ({:ok, “abc”} = {:ok, “hello”}): unmatched pattern (term is unmatched with pattern, raise an runtime error)

For case using pattern in argument of function, if pattern is unmatched with argument, Elixir will check with next function. If no function is matched with the pattern, an runtime error will be raised.

Simple, we can check value of variables like:

a = :an_atom
b = 1_000
c = {:ok, 1}

# ...

# use case 1
:an_atom = a
1_000 = b
{:ok, 1} = c

# use case 2
case a do
 :an_atom ->
  :ok
 _other ->
  :nok
end

case b do
 1_000 ->
  :default
 _ ->
  :user_config
end

# use case 3
check_ok = fn 
  :ok -> true
  _ -> false
end

result = check_ok.(a)

For case check equal value, that usually uses for verifying value, condition for branch code in function/case do.

If variable is complex term example a tuple, list, map. We can discard other values to check one or more values we want.

Example:

list = [1, :a, "hello"]
tuple = {:ok, :a, 1}
map = %{a: 1, b: "hello"}

# check first item of list is 1.
[1|_] = list # or ^list = [1|_]

# check tuple has same values.
copy = ^tuple = {:ok, :a, 1}

# check tuple has same number of elements.
{_, _, _} = tuple

# check map has a key = :a and value of key = 1.
%{a: 1} = map

Extracting value

This feature of pattern matching help us can extract one or more value from a complex term.

Remember, our expected value will bind to variable follow format of our pattern or key in map case.

extract (expression of pattern matching: {:ok, get_value} = {:ok, “hello”}. after matched, we got “hello” value from original term)

Example:

tuple = {:ok, "hello"}
list = [:a, :b, 1, 2]
complex = {:ok, [{:first, 1}, {:second, [1, 2, 3]}]}

# extract second element of tuple
{:ok, value} = tuple

# get first item in list
[firt|_] = list

# get second item in list
[_, second|_] = list

# get value of result
{:ok, [{_, first_value}, {:second, second_value}]} = complex

We can use all key of example for function like:

defmodule Example do
  def process_second([_, second|_]) do
    # do something with second item.
    IO.puts("Value of second item: #{inspect second}")
    second
  end

 def extract_map(%{a: value} = map) do
   # do some thing with value & map.
   IO.puts("key a has value = #{inspect value}")
   value
 end
end

# use cases for pattern matching in function arguments.
list = [1, :default, "hello"]
map = %{a: "hello", b: "world"}

Example.process_second(list)
Example.extract_map(map)

Other common cases for pattern matching with head of function are separating code like:

defmodule Exam do
  def process_result({:error, reason}) do
    # do something with error case.
  end
  def process_result({:ok, result}) do
    # do something with result.
  end
end

For multi patterns in one expression we can declare like:

list = {:ok, {:waiting, [1, 2, 3]}, {:running, [4, 5, 6]}}

{:ok, _, _} = {_, {:waiting, list1}, _} = {_, _, {:running, list2}} = list

# after run, list1 = [1, 2, 3], list2 = [4, 5, 6]

Pattern matching with Binary/Bitstring

This is interesting way to work with binary in Elixir. I wish other languages have this feature.

We have two type of raw data is binary (for work with byte) and bitstring (for work with bit).

Elixir has very convenient way to work with raw data like binary and bitstring. We can construct and matching these easily.

In this topic we just go to how to match a binary or bitstring only.

Binary matching

Example:

# each byte is an integer 8 bit.
bin = <<1, 2, 3, 4, 5>>

# Get first byte
<<first, rest::binary>> = bin

# Get second byte
<<1, second, rest::binary>> = bin

# get 2 bytes in head of binary
<<head::binary-size(2), rest::binary>> = bin

Is it simple right?

Bitstring matching

We can match & get single bit from a raw data (bitstring).

Example:

bit = <<3::4, 1::4, 3::4, 0::4>>

# get first 8 bits from bitstring.
<<first_4::bitstring-size(8), rest::bitstring>> = bit

# get 4 bits after first 4 bits is match to <<3::4>> and store to get_4_bits variable
<<3::4, get_4_bits::bitstring-size(4), _>> = bit

We can easy to process raw data with pattern matching.