Subscribe now

`for` Comprehensions [02.13.2017]

Today I'd like to introduce another piece of syntax to you: for comprehensions. You can think of them like high-powered enumeration helpers. Let's get started.

Project

We'll start a new project to play with these:

mix new for_playground
cd for_playground
vim test/for_playground_test.exs

We'll use the test suite to help us play.

for is the Special Form that lets you play with various comprehensions. The most common are List Comprehensions. Comprehensions let you handle combinations of things. They look like this:

defmodule ForPlaygroundTest do
  use ExUnit.Case

  test "basic for comprehensions" do
    result = for i <- [1, 2, 3], do: i
    assert [1, 2, 3] == result
  end
end

By the way, it's worth mentioning that that in-line do is always possible as a replacement for a do/end block in an Elixir expression. This is equivalent:

defmodule ForPlaygroundTest do
  # ...
  test "basic for comprehensions" do
    result =
      for i <- [1, 2, 3] do
        i
      end
    assert [1, 2, 3] == result
  end
end

The thing on the right of the leftward-facing arrow is known as a generator. Here, our generator is a list.

We can use for comprehensions to combine items. For instance, let's describe a deck of cards:

defmodule ForPlaygroundTest do
  # ...
  test "generating a deck of cards" do
    suits = [:clubs, :diamonds, :hearts, :spades]
    ranks = [2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king, :ace]

    # I'll paste in the list of all cards, to compare against
    all_cards =
      [
        {2, :clubs},
        {3, :clubs},
        {4, :clubs},
        {5, :clubs},
        {6, :clubs},
        {7, :clubs},
        {8, :clubs},
        {9, :clubs},
        {10, :clubs},
        {:jack, :clubs},
        {:queen, :clubs},
        {:king, :clubs},
        {:ace, :clubs},
        {2, :diamonds},
        {3, :diamonds},
        {4, :diamonds},
        {5, :diamonds},
        {6, :diamonds},
        {7, :diamonds},
        {8, :diamonds},
        {9, :diamonds},
        {10, :diamonds},
        {:jack, :diamonds},
        {:queen, :diamonds},
        {:king, :diamonds},
        {:ace, :diamonds},
        {2, :hearts},
        {3, :hearts},
        {4, :hearts},
        {5, :hearts},
        {6, :hearts},
        {7, :hearts},
        {8, :hearts},
        {9, :hearts},
        {10, :hearts},
        {:jack, :hearts},
        {:queen, :hearts},
        {:king, :hearts},
        {:ace, :hearts},
        {2, :spades},
        {3, :spades},
        {4, :spades},
        {5, :spades},
        {6, :spades},
        {7, :spades},
        {8, :spades},
        {9, :spades},
        {10, :spades},
        {:jack, :spades},
        {:queen, :spades},
        {:king, :spades},
        {:ace, :spades}
      ]

    # Then we use a for comprehension to combine the suits and ranks
    result =
      for suit <- suits,
          rank <- ranks,
          do: {rank, suit}

    assert all_cards == result
  end
end

You an also add filters to your comprehensions, to restrict values from appearing in the results. We'll use a range of integers as our generator, keeping only the even ones:

  test "filters in for comprehensions" do
    result =
      for i <- 0..10, rem(i, 2) == 0 do
        i
      end

    assert result == [0, 2, 4, 6, 8, 10]
  end

So far, each of our comprehensions has produced a list. We can collect the values into different structures as well. Let's collect them into a map instead of a list:

  test "collecting results into a map" do
    result =
      # We'll use the `into` option to specify a different term to collect into
      for i <- 0..2, into: %{} do
        # When collecting into maps, you're expected to return a 2-tuple with
        # the key and value.
        {"#{i}", i}
      end

    assert result == %{"0" => 0, "1" => 1, "2" => 2}
  end

The into feature is supported by the Collectable protocol.

To learn more about comprehensions, you can check out the guide on the Elixir site or the documentation for the for special form.

Summary

In today's episode we saw how to use comprehensions in Elixir, both for enumerating simple collections as well as combining and filtering them. I hope you have fun playing with enumerables using comprehensions. See you soon!

Resources