Pattern matching in function heads: don’t go overboard

Posted on June 3, 2018

Elixir’s pattern matching is great, but make sure it’s not reducing your code’s readability. In particular, just because you can match all the variables you’ll need from within the function’s head doesn’t mean you should.

Take this code:

  
  
      def handle_call(:checkout, %{workers: [h | t], monitors: monitors}) do
    # ...
  end

  def handle_call(:checkout, %{workers: [], idle_overflow: [h | t]}) do
    # ...
  end

  def handle_call(:checkout, %{workers: [], idle_overflow: [],
        overflow: overflow, overflow_max: max, worker_sup: sup,
        spec: spec, monitors: monitors})
      when overflow < max do
    # ...
  end

  def handle_call(:checkout, %{workers: [], idle_overflow: [],
      overflow: overflow, overflow_max: max, waiting: waiting}) do
    # ...
  end

  
  

A lot of matching is going on there, but how much of it is to differentiate the function heads, and how much is convenience (i.e. binding for later use)?

By leaving only the matches that differentiate the heads and moving other bindings to the function bodies, readability can be improved (although, sadly, this is poorly demonstrated due to the line wrapping required by this blog format):

  
  
    def handle_call(:checkout, %{workers: [h | t]} = state) do
  %{monitors: monitors} = state
  # ...
end

def handle_call(:checkout, %{workers: [], idle_overflow: [h | t]}) do
  # ...
end

def handle_call(:checkout, %{workers: [], idle_overflow: [],
      overflow: overflow, overflow_max: max} = state)
    when overflow < max do
  %{worker_sup: sup, spec: spec, monitors: monitors} = state
  # ...
end

def handle_call(:checkout, state) do
  %{workers: [], idle_overflow: [], overflow: overflow,
      overflow_max: max, waiting: waiting} = state
  # ...
end

  
  

Of course, I’m not saying that you should always split matching/binding between function head and body. But if you find yourself binding so many variables in your function heads that your code’s readability suffers, take a good look at them. The bindings that aren’t discriminating can most likely be moved to the function’s body, simplifying your code.

Update June 28th, 2018: It turns out José also uses this technique:

I tend to use this rule: if the key is necessary when matching the pattern, keep it in the pattern, otherwise move it to the body. So I end-up with code like this:

  
  
    def some_fun(%{field1: :value} = struct) do
  %{field2: value2, field3: value3} = struct
  ...
end

  
  

Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.