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.

  1. The length of an empty list is 0.
  2. 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