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:
| 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.
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.
| 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:
| 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.
| 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.
| 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 |