Slicing and Dicing with Enum

Notes

A 10-Pack of Bears 🐻

Here's the list of bears we pasted in the video:

def list_bears do
  [
    %Bear{id: 1, name: "Teddy", type: "Brown", hibernating: true},
    %Bear{id: 2, name: "Smokey", type: "Black"},
    %Bear{id: 3, name: "Paddington", type: "Brown"},
    %Bear{id: 4, name: "Scarface", type: "Grizzly", hibernating: true},
    %Bear{id: 5, name: "Snow", type: "Polar"},
    %Bear{id: 6, name: "Brutus", type: "Grizzly"},
    %Bear{id: 7, name: "Rosie", type: "Black", hibernating: true},
    %Bear{id: 8, name: "Roscoe", type: "Panda"},
    %Bear{id: 9, name: "Iceman", type: "Polar", hibernating: true},
    %Bear{id: 10, name: "Kenai", type: "Grizzly"}
  ]
end

As always, feel free to use whatever type of resources you fancy. The actual data really doesn't matter! Hungry? You could use a 10-pack of tacos. 🌮

Warning: Bears love tacos.

Exercise: Delegate DELETE to a Controller Action

In a previous exercise you defined a route that handles a DELETE request. Change the route to delegate to a new function/action in the BearController.

Show Answer

Hide Answer

# in handler.ex

def route(%{method: "DELETE", path: "/bears/" <> _id} = conv) do
  BearController.delete(conv, conv.params)
end

# in bear_controller.ex

def delete(conv, _params) do
  %{ conv | status: 403, resp_body: "Deleting a bear is forbidden!"}
end

Capturing Expressions

In the video we used the & operator to capture named functions. For example, we captured the String.upcase function:

> phrases = ["lions", "tigers", "bears", "oh my"]

> Enum.map(phrases, &String.upcase(&1))
["LIONS", "TIGERS", "BEARS", "OH MY"]

The & capture operator creates an anonymous function that calls String.upcase. The &1 is a placeholder for the first argument passed to the function. It's shorthand for doing this:

> Enum.map(phrases, fn(x) -> String.upcase(x) end)
["LIONS", "TIGERS", "BEARS", "OH MY"]

You can also use the & operator to capture expressions. For example, in the video we saw how to triple a list of numbers by calling map with a list and an anonymous "tripler" function:

> Enum.map([1, 2, 3], fn(x) -> x * 3 end)
[3, 6, 9]

Here's the shorthand way using the & capture operator:

> Enum.map([1, 2, 3], &(&1 * 3))
[3, 6, 9]

Notice we surrounded the expression &1 * 3 with parentheses and captured that expression. The result of &(&1 * 3) is an anonymous function.

Alternatively, you can capture the expression as an anonymous function, bind it to a variable, and then pass the function to the higher-order map function:

> triple = &(&1 * 3)
#Function<6.118419387/1 in :erl_eval.expr/5>

> Enum.map([1, 2, 3], triple)
[3, 6, 9]

Exercise: Capture Functions

Just for extra practice and to build muscle memory, in iex first write an anonymous function that adds two numbers and call it.

Show Answer

Hide Answer

> add = fn(a,b) -> a + b end

> add.(1, 2)
3

Then write a shorthand version that uses the & capture operator.

Show Answer

Hide Answer

> add = &(&1 + &2)

> add.(3, 5)
8

Then look up the documentation for the String.duplicate function. Can you think of two ways to capture it? Try both ways and call the result with your favorite repetitive phrases.

Show Answer

Hide Answer

> repeat = &String.duplicate(&1, &2)

> repeat.("Yo", 2)
"YoYo"

> repeat = &String.duplicate/2

> repeat.("Go", 3)
"GoGoGo"

Exercise: Guard Clause Limitations

In the video we used two "type-check" functions in the guard expressions: is_integer and is_binary. You might be wondering what else you can use in guard expressions. Spend a minute looking through the list of allowed functions and operators.

Tip: Summing

In a previous exercise you used recursion to iterate through a list of numbers and accumulate the total. Well, Enum.sum does that for you. And now you know how it works! Check out the documentation and give it a whirl.

Exercise: Implement map

In a previous exercise you used recursion to triple all the numbers in a given list, like so:

defmodule Recurse do
  def triple([head|tail]) do
    [head*3 | triple(tail)]
  end

  def triple([]), do: []
end

IO.inspect Recurse.triple([1, 2, 3, 4, 5])

The result is a new list of tripled numbers:

[3, 6, 9, 12, 15]

What if you wanted to double, quadruple, or even quintuple all the numbers in the list? Well, you know how to write separate functions for each of those operations.

But you also now know that there's a general way using Enum.map:

> nums = [1, 2, 3, 4, 5]

> Enum.map(nums, &(&1 * 2))
[2, 4, 6, 8, 10]

> Enum.map(nums, &(&1 * 4))
[4, 8, 12, 16, 20]

> Enum.map(nums, &(&1 * 5))
[5, 10, 15, 20, 25]

So how does map work? As the documentation says, it returns a new list where each item is the resulting of invoking the function on each corresponding item of the list. Right, but how does it do that?

Well, you already know that too! You know how to recursively traverse a list. And you know how to create a new list from a head and a tail. And you now know how to invoke an anonymous function. So you can implement your own version of map to validate what you already know!

In the Recurse module (or a stand-alone module of your choosing), define a my_map function that acts just like Enum.map. For example:

> nums = [1, 2, 3, 4, 5]

> Recurse.my_map(nums, &(&1 * 2))
[2, 4, 6, 8, 10]

> Recurse.my_map(nums, &(&1 * 4))
[4, 8, 12, 16, 20]

> Recurse.my_map(nums, &(&1 * 5))
[5, 10, 15, 20, 25]

The my_map functions are similar to the triple functions, but with a slight twist. The my_map functions need to take two arguments: a list and a function. Recursively traverse the list, calling the anonymous function with the head at each step and calling itself (my_map) with the tail.

defmodule Recurse do
  def my_map([head|tail], fun) do
    [fun.(head) | my_map(tail, fun)]
  end

  def my_map([], _fun), do: []
end

Exercise: Reduce the Headers

In a previous video we parsed the request headers by recursively traversing the list of header lines, parsing each line into a key and a value, and accumulating the key-value pairs in a headers map:

def parse_headers([head | tail], headers) do
  [key, value] = String.split(head, ": ")
  headers = Map.put(headers, key, value)
  parse_headers(tail, headers)
end

def parse_headers([], headers), do: headers

Works a treat! And it helped us learn recursion while moving our web server forward.

Now that we've been exposed to the Enum module which abstracts a bunch of common recursive operations behind functions, you've likely envisioned another way. Indeed, the familiar reduce function lives for this type of thing.

Here's an example of how to call it to sum up a list of numbers:

> nums = [1, 2, 3, 4, 5]

> Enum.reduce(nums, 0, fn(x, total_so_far) -> x + total_so_far end)
15

It takes three arguments: a list of values, an initial value (0), and a function that takes two arguments. The first argument to the function is the next element in the list and the second argument is an accumulator. We named it total_so_far, but you'll often see it abbreviated acc for accumulator.

It starts by calling the function with the first element in the list (1) and the initial value (0). The function adds those numbers together and the returned value becomes the accumulator (1). Next the function is called with the second element in the list (2) and the accumulator (1), and the sum (3) become the new value of the accumulator. And so on until it returns the final accumulator value—the sum of 15. It's similiar to reduce in Ruby or JavaScript in that it reduces a list of values down to a single value. How does it work? You know the answer: recursion!

Want to give it a try on a more practical example? In the Parser module, comment the existing parse_headers function clauses and write a new parse_headers function that uses reduce to reduce the list of header lines down to a single map of key-value pairs. You'll call it with a list of header lines, like so:

headers = parse_headers(header_lines)

The initial value will be an empty map %{}. Each time the function is invoked it will get a single header line and the map of key-value pairs accumulated so far. Split the line into a key and a value and put the pair into the accumulated map using Map.put. That last line of the function needs to be Map.put so that the returned list is implicitly returned from the function, becoming the new accumulator.

def parse_headers(header_lines) do
  Enum.reduce(header_lines, %{}, fn(line, headers_so_far) ->
    [key, value] = String.split(line, ": ")
    Map.put(headers_so_far, key, value)
  end)
end

Code So Far

The code for this video is in the enum directory found within the video-code directory of the code bundle.