More Complex List Patterns

Not every list problem can be easily solved by processing one element at a time. Fortunately, the join operator, |, supports multiple values to its left. Thus, you could write

 iex>​ [ 1, 2, 3 | [ 4, 5, 6 ]]
 [1, 2, 3, 4, 5, 6]

The same thing works in patterns, so you can match multiple individual elements as the head. For example, this program swaps pairs of values in a list:

lists/swap.exs
 defmodule​ Swapper ​do
 def​ swap([]), ​do​: []
 def​ swap([ a, b | tail ]), ​do​: [ b, a | swap(tail) ]
 def​ swap([_]), ​do​: ​raise​ ​"​​Can't swap a list with an odd number of elements"
 end

We can play with it in IEx:

 iex>​ c ​"​​swap.exs"
 [Swapper]
 iex>​ Swapper.swap [1,2,3,4,5,6]
 [2, 1, 4, 3, 6, 5]
 iex>​ Swapper.swap [1,2,3,4,5,6,7]
 **​ (RuntimeError) Can't swap a list with an odd number of elements

The third definition of swap matches a list with a single element. This will happen if we get to the end of the recursion and have only one element left. Given that we take two values off the list on each cycle, the initial list must have had an odd number of elements.

Lists of Lists

Let’s imagine we had recorded temperatures and rainfall at a number of weather stations. Each reading looks like this:

 [ timestamp, location_id, temperature, rainfall ]

Our code is passed a list containing a number of these readings, and we want to report on the conditions for one particular location, number 27.

lists/weather.exs
 defmodule​ WeatherHistory ​do
 
 def​ for_location_27([]), ​do​: []
 def​ for_location_27([ [time, 27, temp, rain ] | tail]) ​do
  [ [time, 27, temp, rain] | for_location_27(tail) ]
 end
 def​ for_location_27([ _ | tail]), ​do​: for_location_27(tail)
 
 end

This is a standard recurse until the list is empty stanza. But look at our function definition’s second clause. Where we’d normally match into a variable called head, here the pattern is

 for_location_27([ [ time, 27, temp, rain ] | tail])

For this to match, the head of the list must itself be a four-element list, and the second element of this sublist must be 27. This function will execute only for entries from the desired location. But when we do this kind of filtering, we also have to remember to deal with the case when our function doesn’t match. That’s what the third line does. We could have written

 for_location_27([ [ time, _, temp, rain ] | tail])

but in reality we don’t care what is in the head at this point.

In the same module we define some simple test data:

lists/weather.exs
 def​ test_data ​do
  [
  [1366225622, 26, 15, 0.125],
  [1366225622, 27, 15, 0.45],
  [1366225622, 28, 21, 0.25],
  [1366229222, 26, 19, 0.081],
  [1366229222, 27, 17, 0.468],
  [1366229222, 28, 15, 0.60],
  [1366232822, 26, 22, 0.095],
  [1366232822, 27, 21, 0.05],
  [1366232822, 28, 24, 0.03],
  [1366236422, 26, 17, 0.025]
  ]
 end

We can use that to play with our function in IEx. To make this easier, I’m using the import function. This adds the functions in WeatherHistory to our local name scope. After calling import we don’t have to put the module name in front of every function call.

 iex>​ c ​"​​weather.exs"
 [WeatherHistory]
 iex>​ ​import​ WeatherHistory
 WeatherHistory
 iex>​ for_location_27(test_data)
 [[1366225622, 27, 15, 0.45], [1366229222, 27, 17, 0.468],
  [1366232822, 27, 21, 0.05]]

Our function is specific to a particular location, which is pretty limiting. We’d like to be able to pass in the location as a parameter. We can use pattern matching for this.

lists/weather2.exs
 defmodule​ WeatherHistory ​do
 
 def​ for_location([], _target_loc), ​do​: []
 
»def​ for_location([ [time, target_loc, temp, rain ] | tail], target_loc) ​do
  [ [time, target_loc, temp, rain] | for_location(tail, target_loc) ]
 end
 
 def​ for_location([ _ | tail], target_loc), ​do​: for_location(tail, target_loc)
 
 end

Now the second function fires only when the location extracted from the list head equals the target location passed as a parameter.

But we can improve on this. Our filter doesn’t care about the other three fields in the head—it just needs the location. But we do need the value of the head itself to create the output list. Fortunately, Elixir pattern matching is recursive and we can match patterns inside patterns.

lists/weather3.exs
 defmodule​ WeatherHistory ​do
 
 def​ for_location([], _target_loc), ​do​: []
 
»def​ for_location([ head = [_, target_loc, _, _ ] | tail], target_loc) ​do
  [ head | for_location(tail, target_loc) ]
 end
 
 def​ for_location([ _ | tail], target_loc), ​do​: for_location(tail, target_loc)
 
 end

The key change here is this line:

 def​ for_location([ head = [_, target_loc, _, _ ] | tail], target_loc)

Compare that with the previous version:

 def​ for_location([ [ time, target_loc, temp, rain ] | tail], target_loc)

In the new version, we use placeholders for the fields we don’t care about. But we also match the entire four-element array into the parameter head. It’s as if we said, “Match the head of the list where the second element is matched to target_loc and then match that whole head with the variable head.” We’ve extracted an individual component of the sublist as well as the entire sublist.

In the original body of for_location, we generated our result list using the individual fields:

 def​ for_location([ [ time, target_loc, temp, rain ] | tail], target_loc)
  [ [ time, target_loc, temp, rain ] | for_location(tail, target_loc) ]
 end

In the new version, we can just use the head, making it a lot clearer:

 def​ for_location([ head = [_, target_loc, _, _ ] | tail], target_loc) ​do
  [ head | for_location(tail, target_loc) ]
 end