Chapter 7. Higher-Order Functions and List Comprehensions

Higher-order functions, functions that accept other functions as arguments, are a key place where Erlang’s power really starts to shine. Unlike many languages, Erlang treats higher-order functions as a native and natural part of the language rather than an oddity. List comprehensions apply them in a compact style.

Simple Higher-Order Functions

Way back in Chapter 2, you saw how to use a fun to create a function:

1> Fall_velocity = fun(Distance) -> math:sqrt(2 * 9.8 * Distance) end.
#Fun<erl_eval.6.111823515>
2> Fall_velocity(20).
19.79898987322333
3> Fall_velocity(200).
62.609903369994115

Erlang not only lets you put functions into variables, it lets you pass functions as arguments. This means that you can create functions whose behavior you modify at the time you call it, in much more intricate ways than are normally possible with parameters. A very simple function that takes another function as an argument might look like Example 7-1, which you can find in ch07/ex1-hof.

Example 7-1. An extremely simple higher-order function
-module(hof).
-export([tripler/2]).

tripler(Value, Function) -> 3 * Function(Value).

The argument names are generic, but fit. tripler/2 will take a value and a function as arguments. It runs the value through the function, and multiplies that result by three. In the shell, this might look like the following:

1> c(hof).
{ok,hof}
2> MyFunction=fun(Value)->20*Value end.
#Fun<erl_eval.6.111823515>
3> hof:tripler(6,MyFunction).
360

That defines another simple function taking one argument (and returning that number multiplied by 20), and stores it in the variable MyFunction. Then it calls the hof:tripler/2 function with a value of six and the MyFunction function. In the hof:tripler/2 function, it feeds the Value to the Function, getting back 120. Then it triples that, returning 360.

You can skip assigning the function to a variable if you want, and just include the fun declaration inside the hof:tripler/2 function call:

4> hof:tripler(6,fun(Value)->20*Value end).
360

That may or may not be easier to read, depending on the functions and your expectations. This case is trivially simple, but demonstrates that it works.

Warning

While this is a powerful technique, you can outsmart yourself with it easily. (I do!) Just as with normal code, you need to make sure the number and sometimes the type of your arguments line up. The extra flexibility and power can create new problems if you aren’t careful.

fun has a few other tricks up its sleeve that you should know. You can use a fun to preserve context, even context that has since vanished:

5> X=20.
20
6> MyFunction2=fun(Value)->X * Value end.
#Fun<erl_eval.6.82930912>
7> f(X).
ok
8> X.
* 1: variable 'X' is unbound
9> hof:tripler(6,MyFunction2).
360

Line 5 assigns a variable named X a value, and line 6 uses that variable in a fun. Line 7 obliterates the X variable, as line 8 demonstrates, but line 9 shows that MyFunction2 still remembers that X was 20. Even though the value of X has been flushed from the shell, the fun preserves the value and can act upon it. (This preservation is called a closure.)

You may also want to pass a function from a module, even a built-in module, to your (or any) higher-order function. That’s simple, too:

7> hof:tripler(math:pi(), fun math:cos/1).
-3.0

In this case, the hof:tripler function receives the value pi and a fun, which is the math:cos/1 function from the built-in math module. Since the cosine of pi is -1, the tripler returns -3.0.

Creating New Lists with Higher-Order Functions

Lists are one of the best and easiest places to apply higher-order functions. Applying a function to all the components of a list to create a new list, sort a list, or break a list into smaller pieces is common. And you don’t need to do much work to make it happen: Erlang’s built-in lists module offers a variety of higher-order functions, listed in Appendix A, that take a function and list and do something with them. You can also use list comprehensions to do much of the same work. The lists module may seem easier at first, but as you’ll see, list comprehensions are powerful and concise.

Reporting on a List

The simplest of these functions is foreach/2, which always returns the atom ok. That may sound strange, but foreach/2 is a function you’ll call if and only if you want to do something to the list using side effects—like present the contents of a list to the console. To do that, define a simple function that applies io:format/2, here stored in the variable Print, and a List, and then pass them both to lists:foreach/2:

1> Print = fun(Value) -> io:format("  ~p~n",[Value]) end.
#Fun<erl_eval.6.111823515>
2> List = [1,2,4,8,16,32].
[1,2,4,8,16,32]
3> lists:foreach(Print,List).
  1
  2
  4
  8
  16
  32
ok

The lists:foreach/2 function walked through the list, in order, and called the function in Print with each item of the list as a Value. The io:format/2 function inside of Print presented the list item, slightly indented. When it reached the end of the list, lists:foreach/2 returned the value ok, which the console also displayed.

Note

Most of the demonstrations in this chapter will be operating on that same List variable, containing [1,2,4,8,16,32].

Running List Values Through a Function

You might also want to create a new list based on what a function does with all of the values in the original list. You can square all of the values in a list by creating a function that returns the square of its argument, and passing that to lists:map/2. Instead of returning ok, it returns a new list reflecting the work of the function it was given:

4> Square = fun(Value)->Value*Value end.
#Fun<erl_eval.6.111823515>
5> lists:map(Square,List).
[1,4,16,64,256,1024]

There’s another way to accomplish the same thing, with what Erlang calls list comprehension.

6> [Square(Value) || Value <- List].
[1,4,16,64,256,1024]

That produces the same resulting list, with different (and more flexible) syntax. While you saw the [A | B] syntax in list constructors, a list comprehension uses [A || B] syntax. That extra vertical bar changes the whole way this is interpreted. Instead of being a head and a tail, it’s an expression—here a function—and a rule for extracting the arguments for that function from a list, called a generator.

In this case, the function is the fun you put in the Square variable on line 4. Its argument, Value, is taken on a walk through the List. That arrow (the <-) means “an element of,” or if you want be more active, “comes from.” You can read this list comprehension as “Create a list consisting of squares of a Value, where the Value comes from List.”

Strictly speaking, the expression on the left doesn’t have to be formally declared as a function. You can get the same results with something less formal:

7> [Value * Value || Value <- List].
[1,4,16,64,256,1024]
Note

The multiplication operator (*) is technically a call to the */2 function, but any legal Erlang expression can be on the left of the ||.

Filtering List Values

The lists module offers a few different functions for filtering the content of a list based on a function you provide as a parameter. The most obvious, lists:filter/2, returns a list composed of the members of the original list for which the function returned true. For example, if you wanted to filter a list of integers down to values that could be represented as four binary digits, so numbers 0 or greater but less than 16, you could define a function and store it in Four_bits:

8> Four_bits = fun(Value)-> (Value<16) and (Value>=0) end.
#Fun<erl_eval.6.111823515>

Then, if you apply it to the previously defined List of [1,2,4,8,16,32], you’ll get just the first four values:

9> lists:filter(Four_bits,List).
[1,2,4,8]

Once again, you can create the same effect with a list comprehension. This time, you don’t actually need to create a function, but can instead use a guard-like construct (written without the when) on the right side of the comprehension:

10> [Value || Value <- List, Value<16, Value>=0].
[1,2,4,8]
Note

If you also want a list of values that didn’t match, lists:partition/2, shown in “Splitting Lists”, will return a tuple that contains the matched items in its first element and the unmatched items in its second.

Beyond List Comprehensions

List comprehensions are concise and powerful, but they lack a few key features available in other recursive processing. The only type of result they can return is a list, but there will be many times when you want to process a list and return something else, like a Boolean, a tuple, or a number. List comprehensions also lack support for accumulators, and don’t let you suspend processing completely when certain conditions are met.

You could write your own recursive functions to process lists, but much of the time you’ll find that the lists module already offers a function that takes a function you define and a list and returns what you need.

Testing Lists

Sometimes you just want to know if all the values—or any of the values—in a list meet specific criteria. Are they all of a specific type, or do they have a value that meets certain criteria?

The lists:all/2 and lists:any/2 functions let you test a list against rules you specify in a function. If your function returns true for all of the list values, both of these functions will return true. lists:any/2 will also return true if one or more values in the list results in your function returning true. Both will return false if your function consistently returns false.

Note

lists:all/2 and lists:any/2 don’t necessarily evaluate the entire list; as soon as they hit a value that provides a definitive answer, they’ll stop and return that answer.

11> IsInt = fun(Value) -> is_integer(Value) end.
#Fun<erl_eval.6.111823515>
12> lists:all(IsInt, List).
true
13> lists:any(IsInt, List).
true
14> Compare = fun(Value) -> Value > 10 end.
#Fun<erl_eval.6.111823515>
15> lists:any(Compare, List).
true
16> lists:all(Compare, List).
false

You can think of lists:all/2 as an and function applied to lists; more precisely like andalso because it stops processing as soon as it encounters a false result. Similarly, lists:any/2 is like or, or orelse, in this case stopping as soon as it finds a true result. As long as you need only to test individual values within lists, these two higher-order functions can save you from having to write a lot of recursive code.

Splitting Lists

Filtering lists is useful, but sometimes you want to know what didn’t go through the filter, and sometimes you just want to separate items.

The lists:partition/2 function returns a tuple containing two lists. The first is the list items that met the conditions specified in the function you provided, while the second is the items that didn’t. If the Compare variable is defined as shown in line 14 of the previous demonstration, returning true when a list value is greater than 10, then you can easily split a list into a list of items greater than 10 and a list of items fewer than 10:

17> lists:partition(Compare,List).
{[16,32],[1,2,4,8]}

Sometimes you’ll want to split a list by starting from the beginning—the head—and stopping when a list value no longer meets a condition. The lists:takewhile/2 and lists:dropwhile/2 functions create a new list that contains the parts of an old list before or after encountering a boundary condition. These functions aren’t filters, and to make that clear, the examples use a different list than the rest of the examples in this chapter:

18> Test=fun(Value) -> Value < 4 end.
#Fun<erl_eval.6.111823515>
19> lists:dropwhile(Test, [1,2,4,8,4,2,1]).
[4,8,4,2,1]
20> lists:takewhile(Test, [1,2,4,8,4,2,1]).
[1,2]

Both functions run through a list from head to tail and stop when they reach a value for which the function you provide as the first argument returns false. The lists:dropwhile/2 function returns what’s left of the list, including the value that flunked the test. It does not, however, filter out later list entries that it might have dropped if they had appeared earlier in the list. The lists:takewhile/2 function returns what was already processed, not including the value that flunked the test.

Folding Lists

Adding an accumulator to list processing lets you turn lists into much more than other lists, and opens the door to much more sophisticated processing. Erlang’s lists:foldl/3 and lists:foldr/3 functions let you specify a function, an initial value for an accumulator, and a list. Instead of the one-argument functions you’ve seen so far, you need to create a two-argument function, accepting the current value in the list traversal and the accumulator. The result of that function will become the new value of the accumulator.

Defining a function that works within the folding functions looks a little different, because of the two arguments:

21> Divide=fun(Value, Accumulator) -> Value / Accumulator end.
#Fun<erl_eval.6.111823515>

This function divides its first argument—to be the Value coming from the list—by its second, the Accumulator passed to it by the function doing the folding.

Folding has one other key twist. You can choose whether you want the function to traverse the list from head to tail, with lists:foldl/3, or from tail to head, with lists:foldr/3. foldl means “fold from left to right,” and foldr means “fold from right to left.” If order doesn’t change the result, you should go with lists:foldl/3, as its implementation is tail-recursive and more efficient in most situations.

The Divide function is one of those cases that will produce very different results depending on the direction in which you process the list (and the initial accumulator value). In this case, folding also produces different results than you might expect in a simple division. Given the usual List of [1,2,4,8,16,32], it seems like going from left to right will produce 1/2/4/8/16/32, and going from right to left will produce 32/16/8/4/2/1, at least if you use an initial accumulator of 1. The functions don’t produce those results, however:

22> 1/2/4/8/16/32.
3.0517578125e-5
23> lists:foldl(Divide,1,List).
8.0
24> 32/16/8/4/2/1.
0.03125
25> lists:foldr(Divide,1,List).
0.125

This code seems too simple to have a bug, so what’s going on? Table 7-1 walks through the calculations for lists:foldl(Divide,1,List), and Table 7-2 walks through lists:foldr(Divide,1,List) step by step.

Table 7-1. Recursive division of a list forwards with foldl/3
Value from List Accumulator Result of Division

1

1

1

2

1 (1/1)

2

4

2 (2/1)

2

8

2 (4/2)

4

16

4 (8/2)

4

32

4

8

Table 7-2. Recursive division of a list backwards with foldr/3
Value from List Accumulator Result of Division

32

1

32

16

32 (32/1)

0.5

8

0.5 (32/16)

16

4

16 (8/0.5)

0.25

2

0.25 (4/16)

8

1

8

0.125

Moving through a list step-by-step produces very different values. In this case, the simple Divide function’s behavior changes drastically above and below the value 1, and combining that with walking through a list item by item yields results that might not be precisely what you expected.

Note

The result of the foldl is the same as 32/(16/(8/(4/(2/(1/1))))), while the result of the foldr is the same as 1/(2/(4/(8/(16/(32/1))))). The parentheses in those expressions perform the same restructuring as the fold, and the concluding 1 in each is where the initial accumulator value fits in.

Folding is an incredibly powerful operation. This simple if slightly weird example just used a single value, a number, as an accumulator. If you use a tuple as the accumulator, you can store all kinds of information about a list as it passes by, and even perform multiple operations. You probably won’t want to try to define the functions you use for that as one-liners, but the possibilities are endless.

Note

You can learn more about working with higher-order functions in Chapter 9 of Erlang Programming (O’Reilly); Section 4.3 of Programming Erlang, 2nd Edition (Pragmatic); Section 2.7 of Erlang and OTP in Action (Manning); and Chapter 6 of Learn You Some Erlang For Great Good! (No Starch Press). List comprehensions are in Chapter 9 of Erlang Programming (O’Reilly); Section 4.5 of Programming Erlang, 2nd Edition (Pragmatic); Section 2.9 of Erlang and OTP in Action (Manning); and Chapter 1 of Learn You Some Erlang For Great Good! (No Starch Press).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset