Elixir 1.3
Chapter 7
Lists and Recursion
Heads and Tails
[3] == [ 3 | [] ]
[2, 3] == [ 2 | [ 3 | [] ] ]
[1, 2, 3] == [ 1 | [ 2 | [ 3 | [] ] ] ]
iex> [ 1 | [ 2 | [ 3 | [] ] ] ]
[1, 2, 3]
iex> [ head | tail ] = [ 1, 2, 3 ]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]
A list may either be empty or consist of a head and a tail. The head contains a value and the tail is itself a list.
We represent the empty list like this: [ ].
We can split a list between the head and tail using the pipe character |.
Using Head and Tail to Process a List
defmodule MyList do
def len([]), do: 0
def len([head|tail]), do: 1 + len(tail)
end
# theoretical call
len([11,12,13,14,15])
= 1 + len([12,13,14,15])
= 1 + 1 + len([13,14,15])
= 1 + 1 + 1 + len([14,15])
= 1 + 1 + 1 + 1 + len([15])
= 1 + 1 + 1 + 1 + 1 + len([])
= 1 + 1 + 1 + 1 + 1 + 0
= 5
Let’s find the length of a list using a recursive head tail method.
- The length of an empty list is 0.
- The length of a list is 1 plus the length of that list’s tail.
Using Head and Tail to Process a List
defmodule MyList do
def len([]), do: 0
def len([head|tail]), do: 1 + len(tail)
end
iex> c "mylist.exs"
...mylist.exs:3: variable head is unused
[MyList]
iex> MyList.len([])
0
iex> MyList.len([11,12,13,14,15])
5
Let’s try our code to see if theory works in practice:
Notice the compiler warning. This is because we are never using head. An easy way to fix this is to prefix any unused variables with an underscore _.
Using Head and Tail to Process a List
defmodule MyList do
def len([]), do: 0
def len([_head | tail]), do: 1 + len(tail)
end
iex> c "mylist1.exs"
[MyList]
iex> MyList.len([1,2,3,4,5])
5
iex> MyList.len(["cat", "dog"])
2
Using Head and Tail to Build a list
defmodule MyList do
def square([]), do: []
def square([ head | tail ]), do: [ head*head | square(tail) ]
end
iex> c "mylist1.exs"
[MyList]
iex> MyList.square [] # this calls the 1st definition
[]
iex> MyList.square [4,5,6] # and this calls the 2nd
[16, 25, 36]
Let’s write a function that takes a list of numbers and returns a new list containing the square of each.
Using Head and Tail to Build a list
defmodule MyList do
def add_1([]), do: []
def add_1([ head | tail ]), do: [ head+1 | add_1(tail) ]
end
iex> c "mylist1.exs"
[MyList]
iex> MyList.add_1 [1000]
[1001]
iex> MyList.add_1 [4,6,8]
[5, 7, 9]
Now let’s write a function that takes a list of numbers and adds 1 to every element in the list.
Creating a Map Function
defmodule MyList do
def map([], _func), do: []
def map([ head | tail ], func), do: [ func.(head) | map(tail, func) ]
end
iex> c "mylist1.exs"
[MyList]
iex> MyList.map [1,2,3,4], fn (n) -> n*n end
[1, 4, 9, 16]
iex> MyList.map [1,2,3,4], fn (n) -> n+1 end
[2, 3, 4, 5]
iex> MyList.map [1,2,3,4], fn (n) -> n > 2 end
[false, false, true, true]
iex> MyList.map [1,2,3,4], &(&1 + 1)
[2, 3, 4, 5]
iex> MyList.map [1,2,3,4], &(&1 > 2)
[false, false, true, true]
Let’s generalize our last two functions and
create a map function.
Keeping Track of Values During Recursion
What if we want to sum all the value together? We need a way to remember the partial sum.
In terms of a recursive structure, it’s easy:
- sum([]) → 0
- sum([ head |tail ]) → total + sum(tail)
But this assumes that total is a global variable which doesn't work with our immutable state.
But we can pass the state in a function’s parameter.
Keeping Track of Values During Recursion
defmodule MyList do
def sum([], total), do: total
def sum([ head | tail ], total), do: sum(tail, head+total)
end
Our sum function now has two parameters, the list and the total so far. In the recursive call, we pass it the list’s tail and increment the total by the value of the head.
These types of functions maintain an invariant, a condition that is true on return from any call
(or nested call).
When the list becomes empty the total will be the value we want
Keeping Track of Values During Recursion
defmodule MyList do
def sum([], total), do: total
def sum([ head | tail ], total), do: sum(tail, head+total)
end
iex> c "sum.exs"
[MyList]
iex> MyList.sum([1,2,3,4,5], 0)
15
iex> MyList.sum([11,12,13,14,15], 0)
65
Having to remember that extra zero is a little tacky, so the convention in Elixir is to hide it.
defmodule MyList do
def sum(list), do: _sum(list, 0)
# private methods
defp _sum([], total), do: total
defp _sum([ head | tail ], total), do: _sum(tail, head+total)
end
Generalizing Our Sum Function
General use function that that reduces to a value. reduce(collection, initial_value, fun)
-
reduce([], value, _) → value
-
reduce([ head |tail ], value, fun) → reduce(tail, fun.(head, value), fun)
defmodule MyList do
def reduce([], value, _) do
value
end
def reduce([head | tail], value, func) do
reduce(tail, func.(head, value), func)
end
end
iex> c "reduce.exs"
[MyList]
iex> MyList.reduce([1,2,3,4,5], 0, &(&1 + &2))
15
iex> MyList.reduce([1,2,3,4,5], 1, &(&1 * &2))
120
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.
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.
More Complex List Patterns
Let's swap 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
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
Lists of Lists
Let’s imagine we had recorded temperatures and rainfall at a number of weather stations.
[ timestamp, location_id, temperature, rainfall ]
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
Lists of Lists
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
iex> c "weather.exs"
[WeatherHistory]
iex> import WeatherHistory
nil
iex> for_location_27(test_data)
[[1366225622, 27, 15, 0.45], [1366229222, 27, 17, 0.468],
[1366232822, 27, 21, 0.05]]
Lists of Lists
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 do better. 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.
Luckily Elixir pattern matching is recursive and we can match patterns inside patterns.
Lists of Lists
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
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.”
The List Module in Action
#
# Concatenate lists
#
iex> [1,2,3] ++ [4,5,6]
[1, 2, 3, 4, 5, 6]
#
# Flatten
#
iex> List.flatten([[[1], 2], [[[3]]]])
[1, 2, 3]
#
# Folding (like reduce, but can choose direction)
#
iex> List.foldl([1,2,3], "", fn value, acc -> "#{value}(#{acc})" end)
"3(2(1()))"
iex> List.foldr([1,2,3], "", fn value, acc -> "#{value}(#{acc})" end)
"1(2(3()))"
#
# Updating in the middle (not a cheap operation)
#
iex> list = [ 1, 2, 3 ]
[ 1, 2, 3 ]
iex> List.replace_at(list, 2, "buckle my shoe")
[1, 2, "buckle my shoe"]
The List Module in Action
#
# Accessing tuples within lists
#
iex> kw = [{:name, "Dave"}, {:likes, "Programming"}, {:where, "Dallas", "TX"}]
[{:name, "Dave"}, {:likes, "Programming"}, {:where, "Dallas", "TX"}]
iex> List.keyfind(kw, "Dallas", 1)
{:where, "Dallas", "TX"}
iex> List.keyfind(kw, "TX", 2)
{:where, "Dallas", "TX"}
iex> List.keyfind(kw, "TX", 1)
nil
iex> List.keyfind(kw, "TX", 1, "No city called TX")
"No city called TX"
iex> kw = List.keydelete(kw, "TX", 2)
[name: "Dave", likes: "Programming"]
iex> kw = List.keyreplace(kw, :name, 0, {:first_name, "Dave"})
[first_name: "Dave", likes: "Programming"]
Get Friendly with Lists
Lists are the natural data structure to use when you have a stream of values to handle.
You’ll use them to
- parse data
- handle collections of values
- record the results of a series of function calls
It’s worth spending a while getting comfortable with them.
Thank you!
Programming Elixir 1.3 Chapter 07
By Dustin McCraw
Programming Elixir 1.3 Chapter 07
- 998