Example code: map.erl, filter.erl

Anonymous Functions

Many languages, including Clojure, Scala, and Ruby, have the ability to define "anonymous" blocks of code. An important use of such code blocks is to transform a series of values from a collection (e.g., passing an anonymous function to the Clojure map function.)

Erlang supports anonymous functions. Because Erlang is dynamically-typed, an anonymous function can be assigned to a variable (Clojure allows this as well). You can think of an anonymous function as being a value, in much the same way that numbers, lists, and tuples are values in Erlang. Like all other values, they can be passed to functions and returned from functions.

Example:

1> Add1 = fun(N) -> N+1 end.
#Fun
2> Add1(3).
4

Here, we defined a function of one parameter N that adds 1 to its parameter and returns the result.

More interesting example:

3> AddN = fun(N) -> fun(X) -> N+X end end.
#Fun
4> Add12 = AddN(12).
#Fun
5> Add12(11).
23

In this example, the AddN function takes a parameter N and returns a function that adds N to its parameter X. By calling AddN with the argument value 12, we create a function that adds 12 to its parameter.

The concept of returning a function from a function is called currying.

Transforming Lists

Anonymous functions can be applied to lists to select or transform the values in the list.

One way to transform a list is to map a function onto each element of the list, producing a list of transformed values as a result.

Here is a possible implementation of a map function:

-module(map).
-export([map/2]).

map(_, []) -> [];
map(F, [First|Rest]) -> [F(First) | map(F, Rest)].

The implementation is quite simple. The base case is an empty list, where the result is simply the empty list. In the recursive case, the function F is applied to the first element of the list, and prepended onto the list that results from recursively applying F to the rest of the list.

Testing it on a list:

8> map:map(fun(N) -> N*2 end, [1, 2, 3]).
[2,4,6]

Note that the implementation of map above is not tail-recursive. Here is a tail-recursive version:

-module(map).
-export([map/2]).

mapwork(_, [], Accum) -> lists:reverse(Accum);
mapwork(F, [First|Rest], Accum) -> mapwork(F, Rest, [F(First)|Accum]).

map(F, L) -> mapwork(F, L, []).

Because the accumulator builds the result list with the transformed elements in reverse order, we apply the built-in lists:reverse function before returning the final result.

Another useful list-transformation technique is filtering a list to retain or discard elements matching a specified predicate:

-module(filter).
-export([retain/2, discard/2]).

filterwork(_, [], _, Accum) -> lists:reverse(Accum);
filterwork(F, [First|Rest], Keep, Accum) ->
  Test = F(First),
  if
    (Test and Keep) or (not Test and not Keep) ->
       filterwork(F, Rest, Keep, [First | Accum]);
    true -> filterwork(F, Rest, Keep, Accum)
  end.

retain(F, List) -> filterwork(F, List, true, []).

discard(F, List) -> filterwork(F, List, false, []).

Examples of using these functions:

18> filter:retain(fun(N) -> N > 4 end, [1, 2, 3, 4, 5, 6, 7, 8]).
[5,6,7,8]
19> filter:discard(fun(N) -> N > 4 end, [1, 2, 3, 4, 5, 6, 7, 8]).
[1,2,3,4]

Built-in versions

It's interesting to build our own list-transformation functions, but in practice it's better to use the built-in implementations, which are list::map, lists:takewhile, and lists:dropwhile.

List Comprehensions

List comprehensions are a concise syntax for describing transformations of one or more lists.

In the following examples, the variable List is defined as:

List = [1, 2, 3, 4, 5, 6, 7, 8].

Example: double each element in a list:

26> [N * 2 || N <- List].
[2,4,6,8,10,12,14,16]

Read this as "select elements N from List, and generate a new list by adding elements of the form N*2".

Example: get all elements greater than 4:

27> [N || N <- List, N > 4].
[5,6,7,8]

Here, we've specified an additional clause N>4 to restrict which elements of List are used to generate the result list.

Example: double all elements greater than 4:

28> [N*2 || N <- List, N > 4].
[10,12,14,16]

In this example, we both selected and transformed the input list.