The lists module
map, filter, foldl, sum, sort and seq: the toolbox that does the walking for you, once you can read the two clauses it is built from.
The walk you no longer write
Every function in the lists module is a list walk with two clauses inside it, and the module exists so you do not have to write those clauses again. lists:map(Fun, List) applies a function to each element and returns the results as a new list. lists:filter(Fun, List) keeps the elements for which the function returns true.
lists:foldl(Fun, Start, List) is the one to learn properly. It carries a value through the list: the function receives one element and the value so far, and returns the value so far for the next element. Adding up is a fold with 0 as the start, and joining strings is a fold with the empty string as the start.
The order of the arguments is worth memorising: the function comes first in all three. That is not an accident of style; it is what makes them read well when chained or passed around elsewhere.
Ready-made questions
lists:sum adds a list of numbers in one call and lists:sort puts a list in order. lists:seq(1, 5) builds the list of whole numbers from one to five, which is the nearest thing Erlang has to counting. lists:reverse, lists:member, lists:length and lists:max cover most of the rest of what a program asks about a list.
These are ordinary functions, not syntax, so they take a list and give back a value like anything else. A program that uses them is usually shorter than the same program written with hand-written recursion, and shorter still when the calls are chained one step per line.
lists:zip turns two lists into one list of pairs, stopping at the shorter one, and lists:partition splits a list into the elements that pass and the ones that fail. Both are ordinary walks with a name, and both save a hand-written recursion whose only job is to rearrange a list into the shape the next call wants.
The library also tells you when a list is empty: lists:sum([]) is 0 and lists:max([]) fails with a function clause error, because there is no largest element of nothing. The difference is deliberate, and it is worth knowing which of the two a report can safely call.
Folding is the general case
If you are unsure which of these to reach for, try a fold. Counting, summing, building a new list, finding a maximum: each is a fold with a different starting value and a different two-line function. Map and filter are themselves folds with a habit, and the standard library simply gives the habits names.
Six questions, one list
-module(prog).
-export([main/1]).
main(_) ->
Nums = lists:seq(1, 5),
Double = fun(X) -> X * 2 end,
Even = fun(X) -> X rem 2 == 0 end,
Times = fun(X, A) -> X * A end,
io:format("~p~n", [Nums]),
io:format("doubled ~p~n", [lists:map(Double, Nums)]),
io:format("evens ~p~n", [lists:filter(Even, Nums)]),
io:format("sum ~p~n", [lists:sum(Nums)]),
io:format("product ~p~n", [lists:foldl(Times, 1, Nums)]),
io:format("sorted ~p~n", [lists:sort([3, 1, 2])]).The library does the walking
Run it and check the product against the numbers by hand. Then add a call to `lists:max` and a filter that keeps the numbers above 3.
In lists:foldl(Fun, Start, List) the function takes the element first and the value so far second. Getting those two the wrong way round is the usual first mistake, and the answer comes out as a strange list rather than a number.
foldl walks forward, foldr walks backward
A fold is a way to replace the two clauses of a list walk with one function and a starting value. lists:foldl takes the elements from the left: it calls the function with the first element and the start, then with the second element and that answer, and so on. The accumulator is always the second argument of the function.
lists:foldr starts at the right and works back. The difference is easiest to see when building a list: lists:foldr(fun(X, Acc) -> [X | Acc] end, [], List) keeps the original order, while the same function passed to foldl reverses it. Reach for foldr when the shape of the answer grows to the right.
map, filter and foreach do different jobs
lists:map transforms every element and returns a list of the same length. lists:filter keeps some elements and returns a shorter list. lists:foreach applies a function for its effect and returns the atom ok; it is the one to use when the point is printing or sending, not collecting.
A list comprehension writes the map-and-filter pair in one expression: [X * 2 || X <- List, X > 2] takes each X from List, keeps the ones above 2, and doubles them. It is not a different mechanism, only a different notation for the same walk, and either form is fine when it reads clearly.
Three library mistakes are common. A fold function takes two arguments, the element and the accumulator, so a one-argument fun fails when the fold calls it. Swapping those two arguments gives a strange answer instead of an error, so check the order. And lists:sort returns a new list; it does not rearrange the one you passed in.
Try these next
- Build the same list with
foldlandfoldrand compare the order of the results. - Use a list comprehension to keep the scores above 80 and double them.
- Replace a
lists:mapthat only prints withlists:foreachand explain why the return value changes.
Names, scores and an average
Run it and check the average by hand. Then add a fourth name and score, and print the names of the students who scored below 80.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
In lists:foldl(Fun, 0, List), what does Fun receive each time?
