Code News

Watch the Coolest, Scariest Display Flights of the 2017 Paris Air Show

Netflix's New Gawker vs. Hulk Hogan Doc Has Us All Conflicted

How to Work With Elixir Comprehensions

  • Elixir is a very young programming language (emerged in 2011), but it is gaining popularity. I was initially interested in this language because when using it you can look at some common tasks programmers usually solve from a different angle. For instance, you can find out how to iterate over collections without the for cycle, or how to organize your code without classes.

    Elixir has some very interesting and powerful features that may be hard to get your head around if you came from the OOP world. However, after some time it all starts to make sense, and you see how expressive the functional code can be. Comprehensions are one such feature, and this article I will explain how to work with them.

    Comprehensions and Mapping

    Generally speaking, a list comprehension is a special construct that allows you to create a new list based on existing ones. This concept is found in languages like Haskell and Clojure. Erlang also presents it and, therefore, Elixir has comprehensions as well.

    You might ask how comprehensions are different from the map/2 function, which also takes a collection and produces a new one? That would be a fair question! Well, in the simplest case, comprehensions do pretty much the same thing. Take a look at this example:

    defmodule MyModule do def do_something(list) do list |> Enum.map(fn(el) -> el * 2 end) end end MyModule.do_something([1,2,3]) |> IO.inspect # => [2,4,6]

    Here I am simply taking a list with three numbers and producing a new list with all the numbers multiplied by 2. The map call can be further simplified as Enum.map( &(&1 * 2) ).

    The do_something/1 function can now be rewritten using a comprehension:

    def do_something(list) do for el <- list, do: el * 2 end

    This is what a basic comprehension looks like and, in my opinion, the code is a bit more elegant than in the first example. Here, once again, we take each element from the list and multiply it by 2. The el <- list part is called a generator, and it explains how exactly you wish to extract the values from your collection.

    Note that we are not forced to pass a list to the do_something/1 function—the code will work with anything that is enumerable:

    defmodule MyModule do def do_something(collection) do for el <- collection, do: el * 2 end end MyModule.do_something((1..3)) |> IO.inspect

    In this example, I am passing a range as an argument.

    Comprehensions work with binstrings as well. The syntax is slightly different as you need to enclose your generator with << and >>. Let's demonstrate this by crafting a very simple function to "decipher" a string protected with a Caesar cipher. The idea is simple: we replace each letter in the word with a letter a fixed number of positions down the alphabet. I'll shift by 1 position for simplicity:

    defmodule MyModule do def decipher(cipher) do for << char <- cipher >>, do: char - 1 end end MyModule.decipher("fmjyjs") |> IO.inspect # => 'elixir'

    This is looking pretty much the same as the previous example except for the << and >> parts. We take a code of each character in a string, decrement it by one, and construct a string back. So the ciphered message was "elixir"!

    But still, there is more than that. Another useful feature of comprehensions is the ability to filter out some elements.

    Comprehensions and Filtering

    Let's further extend our initial example. I am going to pass a range of integers from 1 to 20, take only the elements that are even, and multiply them by 2:

    defmodule MyModule do require Integer def do_something(collection) do collection |> Stream.filter( &Integer.is_even/1 ) |> Enum.map( &(&1 * 2) ) end end MyModule.do_something( (1..20) ) |> IO.inspect

    Here I had to require the Integer module to be able to use the is_even/1 macro. Also, I am using Stream to optimize the code a bit and prevent the iteration from being performed twice.

    Now let's rewrite this example with a comprehension again:

    def do_something(collection) do for el <- collection, Integer.is_even(el), do: el * 2 end

    So, as you see, for can accept an optional filter to skip some elements from the collection.

    You are not limited to only one filter, so the following code is legit as well:

    def do_something(collection) do for el <- collection, Integer.is_even(el), el < 10, do: el * 2 end

    It will take all even numbers less than 10. Just don't forget to delimit filters with commas.

    The filters will be evaluated for each element of the collection, and if evaluation returns true, the block is executed. Otherwise, a new element is taken. What's interesting is that generators can also be used to filter out elements by using when:

    def do_something(collection) do for el when el < 10 <- collection, Integer.is_even(el), do: el * 2 end

    This is very similar to what we do when writing guard clauses:

    def do_something(x) when is_number(x) do # ... endComprehensions With Multiple Collections

    Now suppose we have not one but two collections at once, and we'd like to produce a new collection. For example, take all even numbers from the first collection and odd from the second one, and then multiply them:

    defmodule MyModule do require Integer def do_something(collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1 * el2 end end MyModule.do_something( (1..20), (5..10) ) |> IO.inspect

    This example illustrates that comprehensions may work with more than one collection at once. The first even number from collection1 will be taken and multiplied by each odd number from collection2. Next, the second even integer from collection1 will be taken and multiplied, and so on. The result will be: 

    [10, 14, 18, 20, 28, 36, 30, 42, 54, 40, 56, 72, 50, 70, 90, 60, 84, 108, 70, 98, 126, 80, 112, 144, 90, 126, 162, 100, 140, 180]

    What's more, the resulting values are not required to be integers. For instance, you may return a tuple containing integers from the first and the second collections:

    defmodule MyModule do require Integer def do_something(collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: {el1,el2} end end MyModule.do_something( (1..20), (5..10) ) |> IO.inspect # => [{2, 5}, {2, 7}, {2, 9}, {4, 5}...]Comprehensions With the "Into" Option

    Up to this point, the final result of our comprehension was always a list. This is, actually, not mandatory either. You can specify an into parameter that accepts a collection to contain the resulting value. 

    This parameter accepts any structure that implements the Collectable protocol, so for example we may generate a map like this:

    defmodule MyModule do require Integer def do_something(collection1, collection2) do for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), into: Map.new, do: {el1,el2} end end MyModule.do_something( (1..20), (5..10) ) |> IO.inspect # => %{2 => 9, 4 => 9, 6 => 9...}

    Here I simply said into: Map.new, which can be also replaced with into: %{}. By returning the {el1, el2} tuple, we basically set the first element as a key and the second as the value.

    This example is not particularly useful, however, so let's generate a map with a number as a key and its square as a value:

    defmodule MyModule do def do_something(collection) do for el <- collection, into: Map.new, do: {el, :math.sqrt(el)} end end squares = MyModule.do_something( (1..20) ) |> IO.inspect # => %{1 => 1.0, 2 => 1.4142135623730951, 3 => 1.7320508075688772,...} squares[3] |> IO.puts # => 1.7320508075688772

    In this example I am using Erlang's :math module directly, as, after all, all modules' names are atoms. Now you can easily find the square for any number from 1 to 20.

    Comprehensions and Pattern Matching

    The last thing to mention is that you can perform pattern matching in comprehensions as well. In some cases it may come in pretty handy.

    Suppose we have a map containing employees' names and their raw salaries:

    %{"Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30}

    I want to generate a new map where the names are downcased and converted to atoms, and salaries are calculated using a tax rate:

    defmodule MyModule do @tax 0.13 def format_employee_data(collection) do for {name, salary} <- collection, into: Map.new, do: {format_name(name), salary - salary * @tax} end defp format_name(name) do name |> String.downcase |> String.to_atom end end MyModule.format_employee_data( %{"Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30} ) |> IO.inspect # => %{alice: 39.15, bill: 34.8, jim: 26.1, joe: 43.5}

    In this example we define a module attribute @tax with an arbitrary number. Then I deconstruct the data in the comprehension using {name, salary} <- collection. Lastly, format the name and calculate the salary as needed, and store the result in the new map. Quite simple yet expressive.

    Conclusion

    In this article we have seen how to use Elixir comprehensions. You may need some time to get accustomed to them. This construct is really neat and in some situations can fit in much better than functions like map and filter. You can find some more examples in Elixir's official docs and the getting started guide.

    Hopefully, you've found this tutorial useful and interesting! Thank you for staying with me, and see you soon.

    6 days 16 min ago

Google Unveils an AI Investment Fund. It's Betting on an App Store for Algorithms.

ORWL's Ultra-Secure PC Self Destructs if Someone Messes With It

No One Has the Data to Prevent the Next Flint

Watch SpaceX Fire Off Its Second Flight-Proven Falcon 9

As Uber Flails, Its Self-Driving Car Research Rolls On

The Senate Health Bill Is a Disaster for the Opioid Crisis

Pages