Elixir from the ground up

Enum and String: the toolbox

The two modules you will use every day. Enum answers questions about collections — map, filter, reduce, sum, count, sort and group — and String tidies text.

Part 8 of 12

Intermediate20 min readBack to Courses

Ask the collection, do not walk it

Enum is a set of functions that take a collection and a small function, and give you an answer. Enum.map turns every item into a new item. Enum.filter keeps the items the small function approves of. Enum.reduce walks the whole collection keeping one running value, which is how a list becomes a single number or a whole new structure. All three take the collection first, and the function comes after the data it works on, which is why they line up in a pipe.

Around those three sit the ones you would otherwise write by hand: Enum.sum, Enum.count, Enum.sort_by, Enum.max_by, Enum.group_by, Enum.member?. Most of them take the collection first, so they line up in a pipe. The result is a program that says what it wants — the odd numbers, the longest word, the orders by town — instead of showing how it walked the list to get there. The names are the documentation: a reader who knows Enum can tell what a line does without following a loop, and the same line works on a list, a range or a map.

An argument that is a function

The small functions you hand to Enum are written with fn and end, or with a short form when they fit on one line. Because Elixir treats a function as an ordinary value, you can also pass a named function directly: &String.upcase/1 means "the function called upcase that takes one argument", and a small unnamed one can be written as &(&1 + 1), where &1 stands for its first argument. The capture form is shortest when the function is one call: &String.upcase/1 names an existing function, while &(&1 + 1) builds a new one.

Keep them short. A small function that needs a name and a paragraph of explanation is a function that deserves to live in a module, where the name and the comment can travel with it.

String, and the tools it holds

The String module is the other toolbox: String.split cuts text into a list, String.trim removes space from the ends, String.upcase and String.downcase change case, String.replace swaps text, String.pad_leading lines numbers up, and String.first takes the first character. Text and lists pass back and forth all day long, which is why these two modules are learned together. A String function never changes the text you pass; it answers a new string, so the original name still holds what it held before.

Two details of this release are worth knowing. Joining a list of strings back into one is Enum.join — there is no String.join here. And an empty value has its own test: String.trim(text) == "" asks whether there is nothing left, because an empty string counts as true in a condition like any other string.

Words, prices and text

words = ["fig", "apple", "pear", "plum"]
four = fn w -> String.length(w) == 4 end

IO.inspect(Enum.map(words, fn w -> String.upcase(w) end))
IO.inspect(Enum.filter(words, four))
IO.inspect(Enum.sort_by(words, fn w -> String.length(w) end))
IO.inspect(Enum.group_by(words, fn w -> String.first(w) end))
IO.inspect(Enum.max_by(words, fn w -> String.length(w) end))
IO.inspect(Enum.join(words, ", "))
IO.inspect(Enum.any?(words, fn w -> w == "pear" end))

prices = [19.0, 4.5, 12.25]
IO.inspect(Enum.sum(prices))
IO.inspect(Enum.count(prices))
IO.inspect(Enum.reduce(prices, 0.0, fn p, acc -> p + acc end))

IO.inspect(String.split("a,b,c", ","))
IO.inspect(String.replace("frog", "f", "d"))
IO.inspect(String.pad_leading("7", 3, "0"))
IO.inspect(String.capitalize("elixir"))

Numbers and words, asked politely

Run it and match each printed answer to the call that produced it. Then print the words longer than three letters, count how many there are, and group the numbers by whether they are even.

Output
Click "Run" to execute your code

Enum.reduce takes three things: the collection, the value to start from, and a function of two arguments — the current item and the running value. Whatever the function returns becomes the running value for the next item, and whatever is left at the end is the answer.

Enum walks, String measures

Enum works on anything enumerable: a list, a map, a range. A map is enumerated as {key, value} pairs, and the order is not something to rely on — sort the pairs when the output has to read a particular way. A range such as 1..5 is enumerable too, so the same functions work without building a list first. A range also costs almost nothing to walk, which is why a million numbers can be summed without a million-item list.

String works on one piece of text at a time. Its functions are UTF-8 aware, so String.length counts characters while byte_size counts bytes, and String.upcase changes letters rather than bytes. Use String for text and Enum for collections; the two meet at String.split, which makes a list, and Enum.join, which makes text again.

Enum.map is for building a new collection; when you only want to print, Enum.each says so and does not build a list you throw away. Enum.reduce takes the collection, the starting value, and a function of item and accumulator — the starting value comes before the function. And a map printed without sorting may surprise you: its order is an implementation detail.

Try these next

  • Split a sentence into words, keep the words longer than four letters, and join them again.
  • Sum the squares of 1 to 10 with Enum.map and Enum.sum.
  • Group a list of words by their first letter and print the groups in a fixed order.

Words in a sentence, measured

Run it and check the counts against the sentence. Then add a word, find the shortest word with `Enum.min_by`, and print every word with its length using `Enum.map_join`.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the difference between Enum.map and Enum.filter?