Quantcast
Channel: Grant Winney
Viewing all articles
Browse latest Browse all 348

Comprehending "List Comprehensions" in Erlang (a comparison with C#'s LINQ)

$
0
0
Comprehending

Any time we learn something new, we tend to make sense of it in relation to what we already know, one thing building on top of another. Since I started learning Erlang a year ago, I find myself frequently drawing comparisons from 8 years of C# knowledge.

The Verbose Way to Iterate a List

One of the tougher concepts to accept was that the value of a variable in Erlang cannot be modified once it's set. Instead of a loop that repeatedly updates a variable, you'll frequently have to make use of recursion.

C# (foreach loop)

For example, this is perfectly okay in C#. It accepts a list of numbers, then returns a new list with the double of any numbers in the original list that were even.

var numbers = new List<int> { 1, 2, 5, 8 };

public static List<int> DoubleEvenNumbers(List<int> numbers)
{
    var result = new List<int>();
    foreach(var n in numbers)
        if (n % 2 == 0)
            result.Add(2 * n);
    return result;  // returns [4, 16]
}

Erlang (recursion)

You can't exactly do that in Erlang, because of the whole "set it one time only" thing. Instead, you could use recursion (more specifically tail-recursion) to solve the same problem.

-module(numbers).
-export([double_even/1]).

double_even(Numbers) ->
    lists:reverse(double_even(Numbers, [])).

double_even([], Acc) ->
    Acc;
double_even([H|T], Acc) when H rem 2 =:= 0 ->
    double_even(T, [H * 2] ++ Acc);
double_even([_|T], Acc) ->
    double_even(T, Acc).

The Succinct Way to Iterate a List

Fortunately, both languages have a shorter (more succinct) way of achieving the same thing.

C# (LINQ)

In C# it's called LINQ. For the uninitiated, LINQ allows you to take an IEnumerable<T> and call a series of extension methods on it, each of which accepts a collection and returns a collection. You can use these to modify the original list - filter it, transform it, etc. Let's rewrite the first example:

var numbers = new List<int> { 1, 2, 5, 8 };

public static List<int> DoubleEvenNumbers(List<int> numbers)
{
    return numbers.Where(x => x % 2 == 0)
                  .Select(x => 2 * x)
                  .ToList();
}

As you can see it reads pretty close to natural language, first filtering out even numbers, then iterating over the collection and multiplying each element by 2. The call to ToList() signals the end of the LINQ expression and executes the series of methods, returning a new collection.

Erlang (list comprehension)

In Erlang, there's a concept called "list comprehensions" that reminds me a lot of LINQ. Rewriting the second example above produces the following, reducing all of our code to a single line.

-module(numbers).
-export([double_even/1]).

double_even(Numbers) ->
    [N * 2 || N <- Numbers, N rem 2 =:= 0].

So what exactly is it doing? Let's break it down.

  • N <- Numbers tells it to iterate over the Numbers collection, storing each value in N
  • Everything to the left of the || is the code we'll execute against each N. In this case, we multiply each one by 2.
  • So far, we'll get a list back that multiples every value by 2, so we need to filter. The N rem 2 =:= 0 part tells it to only execute our code (to the left of ||) on even numbers.
  • When the list is completely iterated over, a new list with the results is returned.

What else can we do with list comprehensions?

They're incredibly flexible. We can provide multiple filters:

double_even(Numbers) ->
    [N * 2 || N <- Numbers, N rem 2 =:= 0, N > 3].

numbers:double_even([1,-2,3,4,5,8]).  % returns [8, 16]

We can provide multiple lists to iterate, and the code will execute against every combination of both lists:

-module(names).
-export([greeting/1]).

greeting(Names) ->
    [Greeting ++ " " ++ Name ++ "!" || Name <- Names, Greeting <- ["Hi","Bye"]].

% names:greeting(["Mary","John","Sue"]). -> ["Hi Mary!","Bye Mary!","Hi John!","Bye John!","Hi Sue!", "Bye Sue!"]

We can even combine both of the above, including any number of filters after each of the lists we're iterating over:

-module(names).
-export([greeting/1]).

greeting(Names) ->
  [Greeting ++ " " ++ Name ++ "!" || Name <- Names, Name =/= "Sue", Greeting <- ["Hi","Bye"], Greeting =/= "Bye"].

% names:greeting(["Mary","John","Sue"]). -> ["Hi Mary!","Hi John!"]

Do you use Erlang, and have you heard of list comprehensions before? Have you found any other hidden uses for them? If so, please share - they're my favorite go-to right now, although I'm trying to avoid making them the golden hammer! They're not always the right tool, but list comprehensions can seriously trim down your code in the right cases. You can read more about them in the official docs and in Learn You Some Erlang.


This is post #17 in my personal challenge to complete 30 Days of Blogging. My goal is to become more comfortable with blogging in a more frequent and informal manner.


Viewing all articles
Browse latest Browse all 348

Trending Articles