Comprehensions

Notes

A Subtle Difference with EEx

If you've used other templating languages, then you may have expected you could use <% (without the equals sign) to run the list comprehension, like so:

<% for bear <- bears do %>
  <li><%= bear.name %> - <%= bear.type %></li>
<% end %>

However, in EEx all expressions that output something to the template must use the equals sign (=). In this case, it's the comprehension that returns the final string that we want to inject into the template, thus the need to use <%= rather than <% in order to have the result output to the template:

<%= for bear <- bears do %>
  <li><%= bear.name %> - <%= bear.type %></li>
<% end %>

Inspecting in EEx Templates

In the videos we used the inspect function to get a representation of the bears list and then output it to the template, like so:

<%= inspect(bears) %>

This inspect function is defined in the Kernel module and it's automatically imported.

Here's a gotcha: Calling the inspect function is fundamentally different from calling the IO.inspect function. Calling inspect returns a string representing its argument whereas calling IO.inspect inspects its argument and writes the result to a device such as the console. So in a template file, you always want to use inspect.

Pattern Matching Comprehensions

Imagine we have a list of tuples where the first element is a person's name and the second element indicates whether they prefer dogs or cats.

> prefs = [ {"Betty", :dog}, {"Bob", :dog}, {"Becky", :cat} ]

Now we'd like to get a list of all the dog lovers. To do that, we can use a pattern to destructure the tuples given to us by the generator, like so

> for {name, :dog} <- prefs, do: name
["Betty", "Bob"]

Similarly, if we just want the cat lovers:

> for {name, :cat} <- prefs, do: name
["Becky"]

Neat!

Turns out there's a more explicit way to do the filtering. Comprehensions have inherent support for a filter expression. For example, here's how to get the dog lovers using a filter expression:

> for {name, pet_choice} <- prefs, pet_choice == :dog, do: name
["Betty", "Bob"]

Notice we've used pattern matching to extract the name and pet_choice, and then added the expression pet_choice == :dog to the right of the generator, separating it from the generator with a comma. If the expression is truthy then the element is selected for inclusion in the resulting list. If the filter expression returns false or nil, then the element is rejected.

For comparison, here's how to get the cat lovers:

> for {name, pet_choice} <- prefs, pet_choice == :cat, do: name
["Becky"]

Taking it a step further, the filter can be any predicate expression that returns truthy or falsey values. And because functions are expressions, we can use a function:

> dog_lover? = fn(choice) -> choice == :dog end

> cat_lover? = fn(choice) -> choice == :cat end

> for {name, pet_choice} <- prefs, dog_lover?.(pet_choice), do: name
["Betty", "Bob"]

> for {name, pet_choice} <- prefs, cat_lover?.(pet_choice), do: name
["Becky"]

Tip: Atomize Keys

Someday you'll be happily programming along with an Elixir map, something like this:

> style = %{"width" => 10, "height" => 20, "border" => "2px"}

And then you'll need to access the values, so you'll do something sorta like this, but more practical:

> area = style["width"] * style["height"]

And then you'll wonder: "Hey, how can I convert all the map keys from strings to atoms so I can access the values like this instead?"

> area = style[:width] * style[:height]  # doesn't work!

And because you're a confident Elixir programmer, you'll check out the Map module documentation before resorting to a mindless StackOverflow search. And you'll likely discover that Map.new lets you create a new map by applying a transformation function to each element of an existing map:

> Map.new(style, fn {key, val} -> {String.to_atom(key), val} end)
%{border: "2px", height: 20, width: 10}

Good on you!

But maybe you're not satisfied with that, especially since you now know about comprehensions. There has to be a way to do the same thing with a comprehension, right? So you give this a try:

> for {key, val} <- style, do: {String.to_atom(key), val}
[border: "2px", height: 20, width: 10]

So close! Unfortunately, you get back a list rather than a map.

Not to worry. There's a handy tip that's not as easy to find in the documentation, but you'll totally dig it once you get a little nudge.

By default, the values returned by the do block of a comprehension are packaged into a list. But you can use the :into option to package the returned values into any Collectable. And a map is a collectable, so we can do this:

> for {key, val} <- style, into: %{}, do: {String.to_atom(key), val}
%{border: "2px", height: 20, width: 10}

Did we mention that comprehensions are powerful?

Tip: Precompiling Templates

In the video we used the EEx.eval_file function to read a template file and evaluate the embedded Elixir using a set of bindings. It's simple and it gets the job done. But if your aim was to write a high-performance web server, then using eval_file isn't a good choice. It has to read the template file from disk, parse it, and evaluate it every time the matching route is called. And that's relatively slow...

It would be faster if we could do all the inefficient stuff once—in other words, precompile the template— and then run a function every time the route is called. Thankfully, the EEx module offers an easy way to do that, too! The EEx.function_from_file/5 macro generates a function definition from the file contents. Take a minute to review the example in the documentation.

Here's how you could use it in the context of our web server. Suppose we had a BearView module that called EEx.function_from_file to generate index and show functions for the index.eex and show.eex file contents respectively:

defmodule Servy.BearView do
  require EEx

  @templates_path Path.expand("../../templates", __DIR__)

  EEx.function_from_file :def, :index, Path.join(@templates_path, "index.eex"), [:bears]

  EEx.function_from_file :def, :show, Path.join(@templates_path, "show.eex"), [:bear]
end

Notice the index function takes a bears argument and the show function takes a bear argument.

At compile time, the index and show functions are generated and their bodies return the pre-compiled template.

Then in our BearController we could call those functions to generate the response body:

def index(conv) do
  bears =
    Wildthings.list_bears()
    |> Enum.sort(&Bear.order_asc_by_name/2)

  %{ conv | status: 200, resp_body: BearView.index(bears) }
end

def show(conv, %{"id" => id}) do
  bear = Wildthings.get_bear(id)

  %{ conv | status: 200, resp_body: BearView.show(bear) }
end

The index action calls the BearView.index/1 function, passing the list of bears. And the show action calls the BearView.show/1 function, passing the bear to show.

That's super fast!

Exercise: Deal A Hand Of Cards

We saw in the video that comprehensions can have multiple generators which effectively act like nested loops. Using multiple generators turns out to be a really succinct, powerful way to combine lists of elements (or any enumerable).

For example, suppose you have the following list of 13 card ranks and a list of 4 suits:

ranks =
  [ "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" ]

suits =
  [ "♣", "♦", "♥", "♠" ]

A deck of playing cards consists of the 52 possible pairs : {"2", "♣"}, {"2", "♦"}, {"2", "♥"}, {"2", "♠"}, {"3", "♣"}, {"3", "♦"}, {"3", "♥"}, {"3", "♠"} and so on. In other words, it's the Cartesian product of the ranks and suits (13 * 4).

Use a comprehension to create a deck consisting of 52 tuples representing the possible playing cards, then print out the resulting list.

Show Answer

Hide Answer

deck = for rank <- ranks, suit <- suits, do: {rank, suit}

IO.inspect deck

Then, to further sharpen your Enum skills, identify and use functions in the Enum module to deal 13 random cards from the deck.

Show Answer

Hide Answer

deck
|> Enum.shuffle
|> Enum.take(13)
|> IO.inspect

Now, rather than dealing a single 13-card hand, use a different Enum function to randomly deal 4 hands of 13 cards each.

Show Answer

Hide Answer

deck
|> Enum.shuffle
|> Enum.chunk_every(13)
|> IO.inspect

Exercise: Reuse Render

Currently the render/3 function is defined in the BearController module. You can imagine wanting to render templates from other controllers as well. To allow for easier reuse, move the render/3 function and the @templates_path definition into a new module named View, for example.

Show Answer

Hide Answer

defmodule Servy.View do
  @templates_path Path.expand("../../templates", __DIR__)

  def render(conv, template, bindings \\ []) do
    content =
      @templates_path
      |> Path.join(template)
      |> EEx.eval_file(bindings)

    %{ conv | status: 200, resp_body: content }
  end
end

Then import the render/3 function into the BearController.

Show Answer

Hide Answer

import Servy.View, only: [render: 3]

In the same way, you can now import render/3 into any controller.

Code So Far

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

Congratulations! You're halfway through the course!

Thanks for checking out these free modules. We've be delighted to have you in the Studio for the rest of the course. Use coupon code "FINISHCOURSE" to save 20%!