Chapter 3. Atoms, Tuples, and Pattern Matching

Erlang programs are at heart a set of message requests and tools for processing them. Erlang provides tools that simplify the efficient handling of those messages, letting you create code that is readable (to programmers at least) while still running efficiently when you need speed.

Atoms

Atoms are a key structure in Erlang. Technically they’re just another type of data, but it’s hard to overstate their impact on Erlang programming style.

Usually, atoms are bits of text that start with a lowercase letter, like ok or earth. They can also contain (though not start with) underscores (_) and at symbols (@), like this_is_a_short_sentence or me@home. If you want more freedom to start with uppercase letters or use spaces, you can put them in single quotes, like 'Today is a good day'. Generally, the one word lowercase form is easier to read.

Atoms have a value—it’s the same as their text. (Remember, the period after hello isn’t part of the atom—it ends the expression.)

1> hello.
hello

That’s not very exciting in itself. What makes atoms exciting is the way that they can combine with other types and Erlang’s pattern matching techniques to build simple but powerful logical structures.

Pattern Matching with Atoms

Erlang used pattern matching to make the examples in Chapter 2 work, but it was very simple. The name of the function was the one key piece that varied, and as long as you provided a numeric argument Erlang knew what you meant. Erlang’s pattern matching offers much more sophisticated possibilities, however, allowing you to match on arguments as well as on function names.

For example, suppose you want to calculate the velocity of falling objects not just on Earth, where the gravitational constant is 9.8 meters per second squared, but on Earth’s moon, where it is 1.6 meters per second squared, and on Mars, where it is 3.71 meters per second squared. Example 3-1, which you can find in ch03/ex1-atoms, shows one way to build code that supports this.

Example 3-1. Pattern matching on atoms as well as function names

-module(drop).
-export([fall_velocity/2]).


fall_velocity(earth, Distance) -> math:sqrt(2 * 9.8 * Distance);

fall_velocity(moon, Distance) -> math:sqrt(2 * 1.6 * Distance);

fall_velocity(mars, Distance) -> math:sqrt(2 * 3.71 * Distance).

It looks like the fall_velocity function gets defined three times here, and it certainly provides three processing paths for the same function. However, because those definitions are separated with semicolons, they are treated as choices—selected by pattern-matching—rather than duplicate definitions. As in English, these pieces are called clauses.

Note

If you use periods instead of semicolons, you’ll get errors like drop.erl:5: function fall_velocity/2 already defined.

Once you have this, you can calculate velocities for objects falling a given distance on Earth, the Earth’s moon, and Mars:

1> c(drop).
{ok,drop}
2> drop:fall_velocity(earth,20).
19.79898987322333
3> drop:fall_velocity(moon,20).
8.0
4> drop:fall_velocity(mars,20).
12.181953866272849

You’ll quickly find that atoms are a critical component for writing readable Erlang code.

Atomic Booleans

Two of Erlang’s atoms have special properties: true and false, representing the boolean logic values of the same names. Erlang will return these atoms if you ask it to compare something:

1> 3<2.
false
2> 3>2.
true
3> 10 == 10.
true

Erlang also has special operators that work on these atoms (and on comparisons that resolve to these atoms):

1> true and true.
true
2> true and false.
false
3> true or false.
true
4> false or false.
false
5> true xor false.
true
6> true xor true.
false
7> not true.
false

The and, or, and xor operators both take two arguments. For and, the result is true if and only if the two arguments are true. For or, the result is true if at least one of the arguments is true. For xor, exclusive or, the result is true if one but not both arguments is true. In all other cases they return false. If you’re comparing expressions more complicated than true and false, it’s wise to put them in parentheses.

Note

There are two additional operators for situations where you don’t want or need to evaluate all of the arguments. The andalso operator behaves like and but evaluates the second argument only if the first one is true. The orelse operator evaluates the second argument only if the first one is false.

The not operator is simpler, taking just one argument. It turns true into false and false into true. Unlike the other boolean operators, which go between their arguments, not goes before its single argument.

If you try to use these operators with any other atoms, you’ll get a bad argument exception.

Note

There are other atoms that often have an accepted meaning, like ok and error, but those are more conventions than a formal part of the language.

Guards

The fall_velocity calculations work fairly well, but there’s still one glitch: if the function gets a negative value for distance, the square root (sqrt) function in the calculation will be unhappy:

5> drop:fall_velocity(earth,-20).
** exception error: bad argument in an arithmetic expression
     in function  math:sqrt/1
        called as math:sqrt(-392.0)
     in call from drop:fall_velocity/2 (drop.erl, line 4)

Since you can’t dig a hole 20 meters down, release an object, and marvel as it accelerates to the surface, this isn’t a terrible result. However, it might be more elegant to at least produce a different kind of error.

In Erlang, you can specify which data a given function will accept with guards. Guards, indicated by the when keyword, let you fine-tune the pattern matching based on the content of arguments, not just their shape. Guards have to stay simple, can use only a very few built-in functions, and are limited by a requirement that they evaluate only data without any side effects, but they can still transform your code.

Note

You can find a list of functions that can safely be used in guards in Appendix A.

Guards evaluate their expressions to true or false, as previously described, and the first one with a true result wins. That means that you can write when true for a guard that always gets called if it is reached, or block out some code you don’t want to call (for now) with when false.

In this simple case, you can keep negative numbers away from the square root function by adding guards to the fall_velocity clauses, as shown in Example 3-2, which you can find at ch03/ex2-guards.

Example 3-2. Adding guards to the function clauses

-module(drop).
-export([fall_velocity/2]).

fall_velocity(earth, Distance) when Distance >= 0  -> math:sqrt(2 * 9.8 * Distance);

fall_velocity(moon, Distance) when Distance >= 0 -> math:sqrt(2 * 1.6 * Distance);

fall_velocity(mars, Distance) when Distance >= 0 -> math:sqrt(2 * 3.71 * Distance).

Note

In Erlang, greater-than-or-equal-to is written >=, and less-than-or-equal-to is written =<. Don’t make them look like arrows.

The when expression describes a condition or set of conditions in the function head. In this case, the condition is simple: the Distance must be greater than or equal to zero. If you compile that code and ask for a negative velocity, the result is different:

5> drop:fall_velocity(earth,-20).
** exception error: no function clause matching
     drop:fall_velocity(earth,-20) (drop.erl, line 12)

Because of the guard, Erlang doesn’t find a function clause that works with a negative argument. The error message may not seem like a major improvement, but as you add layers of code, “not handled” may be a more appealing response than “broke my formula.”

A clearer, though still simple, use of guards might be code that returns an absolute value. Yes, Erlang has a built-in function, abs/1, for this, but Example 3-3 makes clear how this works.

Example 3-3. Calculating absolute value with guards

-module(mathdemo).
-export([absolute_value/1]).

absolute_value(Number) when Number < 0  -> -Number;

absolute_value(Number) when Number == 0  -> 0;

absolute_value(Number) when Number > 0  -> Number.

When mathdemo:absolute_value is called with a negative (less than zero) argument, Erlang calls the first clause, which returns the negation of that negative argument, making it positive. When the argument equals (==) zero, Erlang calls the second clause, returning 0. Finally, when the argument is positive, Erlang calls the third clause, just returning the number. (The first two clauses have processed everything that isn’t positive, so the guard on the last clause is unnecessary and will go away in Example 3-4.)

1> c(mathdemo).
{ok,mathdemo}
2> mathdemo:absolute_value(-20).
20
3> mathdemo:absolute_value(0).
0
4> mathdemo:absolute_value(20).
20

This may seem like an unwieldy way to calculate. Don’t worry—Erlang has simpler logic switches you can use inside of functions. However, guards are critically important to choosing among function clauses, which will be especially useful as you start to work with recursion in Chapter 4.

Erlang runs through the function clauses in the order you list them, and stops at the first one that matches. If you find your information is heading to the wrong clause, you may want to re-order your clauses or fine-tune your guard conditions.

Also, when your guard clause is testing for just one value, you can easily switch to using pattern-matching instead of a guard. This absolute_value function in Example 3-4 does the same thing as the one in Example 3-3.

Example 3-4. Calculating absolute value with guards

absolute_value(Number) when Number < 0  -> -Number;
absolute_value(0) -> 0;
absolute_value(Number) -> Number.

In this case, it’s up to you whether you prefer the simpler form or preserving a parallel approach.

Note

You can also have multiple comparisons in a single guard. If you separate them with semicolons it works like an OR statement, succeeding if any of the comparisons succeeds. If you separate them with commas, it works like an AND statement, and they all have to succeed for the guard to succeed.

Underscoring That You Don’t Care

Guards let you specify more precise handling of incoming arguments. Sometimes you may actually want handling that is less precise, though. Not every argument is essential to every operation, especially when you start passing around complex data structures. You could create variables for arguments and then never use them, but you’ll get warnings from the compiler (which suspects you must have made a mistake) and you may confuse other people using your code who are surprised to find your code cares about only half of the arguments they sent.

You might, for example, decide that you’re not concerned with what planemo (for planetary mass object, including planets, dwarf planets, and moons) a user of your velocity function specifies and you’re just going to use Earth’s value for gravity. Then, you might write something like Example 3-5, from ch03/ex3-underscore.

Example 3-5. Declaring a variable and then ignoring it

-module(drop).
-export([fall_velocity/2]).

fall_velocity(Planemo, Distance) -> math:sqrt(2 * 9.8 * Distance).

This will compile, but you’ll get a warning, and if you try to use it for, say, Mars, you’ll get the wrong answer for Mars.

1> c(drop).
drop.erl:5: Warning: variable 'Planemo' is unused
{ok,drop}
2> drop:fall_velocity(mars, 20).
19.79898987322333

On Mars, that should be more like 12 than 19, so the compiler was right to scold you.

Other times, though, you really only care about some of the arguments. In these cases, you can use a simple underscore (_). The underscore accomplishes two things: it tells the compiler not to bother you, and it tells anyone reading your code that you’re not going to be using that argument. In fact, Erlang won’t let you. You can try to assign values to the underscore, but Erlang won’t give them back to you. It considers the underscore permanently unbound:

3> _ = 20.
20
4> _.
* 1: variable '_' is unbound

If you really wanted your code to be earth-centric and ignore any suggestions of other planemos, you could instead write something like Example 3-6.

Example 3-6. Deliberately ignoring an argument with an underscore

-module(drop2).
-export([fall_velocity/2]).

fall_velocity(_, Distance) -> math:sqrt(2 * 9.8 * Distance).

This time there will be no compiler warning, and anyone who looks at the code will know that first argument is useless.

5> c(drop2).
{ok,drop2}
6> drop2:fall_velocity(you_dont_care, 20).
19.79898987322333

You can use underscore multiple times to ignore multiple arguments. It matches anything for the pattern match, and never binds, so there’s never a conflict.

Note

You can also start variables with underscores—like _Planemo—and the compiler won’t warn if you never use those variables. However, those variables do get bound, and you can reference them later in your code if you change your mind. However, if you use the same variable name more than once in a set of arguments, even if the variable name starts with an underscore, you’ll get an error from the compiler for trying to bind twice to the same name.

Adding Structure: Tuples

Erlang’s tuples let you combine multiple items into a single composite data type. This makes it easier to pass messages between components, letting you create your own complex data types as you need. Tuples can contain any kind of Erlang data, including numbers, atoms, other tuples, and the lists and strings you’ll encounter in later chapters.

Tuples themselves are simple, a group of items surrounded by curly braces:

1> {earth, 20}.
{earth, 20}

Tuples might contain 1 item, or they might contain 100. Two to five seem typical (and useful, and readable). Often (but not always) an atom at the beginning of the tuple indicates what it’s really for, providing an informal identifier of the complex information structure stored in the tuple.

Erlang includes rarely used built-in functions that give you access to the contents of a tuple on an item by item basis. You can retrieve the values of items with the element function, set values in a new tuple with the setelement function, and find out how many items are in a tuple with the tuple_size function.

2> Tuple = {earth, 20}.
{earth,20}
3> element(2, Tuple).
20
4> NewTuple = setelement(2, Tuple, 40).
{earth,40}
5> tuple_size(NewTuple).
2

If you can stick with pattern matching tuples, however, you’ll likely create more readable code.

Pattern Matching with Tuples

Tuples make it easy to package multiple arguments into a single container, and let the receiving function decide what to do with them. Pattern matching on tuples looks much like pattern matching on atoms, except that there is, of course, a pair of curly braces around each set of arguments, as Example 3-7, which you’ll find in ch03/ex4-tuples, demonstrates.

Example 3-7. Encapsulating arguments in a tuple

-module(drop).
-export([fall_velocity/1]).

fall_velocity({earth, Distance}) -> math:sqrt(2 * 9.8 * Distance);

fall_velocity({moon, Distance}) -> math:sqrt(2 * 1.6 * Distance);

fall_velocity({mars, Distance}) -> math:sqrt(2 * 3.71 * Distance).

The arity changes: this version is fall_velocity/1 instead of fall_velocity/2 because the tuple counts as only one argument. The tuple version works much like the atom version but requires the extra curly braces when you call the function as well.

1> c(drop).
{ok,drop}
2> drop:fall_velocity({earth,20}).
19.79898987322333
3> drop:fall_velocity({moon,20}).
8.0
4> drop:fall_velocity({mars,20}).
12.181953866272849

Why would you use this form when it requires a bit of extra typing? Using tuples opens more possibilities. Other code could package different things into tuples—more arguments, different atoms, even functions created with fun(). Passing a single tuple rather than a pile of arguments gives Erlang much of its flexibility, especially when you get to passing messages between different processes.

Processing Tuples

There are many ways to process tuples, not just the simple pattern matching shown in Example 3-7. If you receive the tuple as a single variable, you can do many different things with it. A simple place to start is using the tuple as a pass through to a private version of the function. That part of Example 3-8 may look familiar, as it’s the same as the fall_velocity/2 function in Example 3-2. (You can find this at ch03/ex5-tuplesMore.)

Example 3-8. Encapsulating arguments in a tuple and passing them to a private function

-module(drop).
-export([fall_velocity/1]).

fall_velocity({Planemo, Distance}) -> fall_velocity(Planemo, Distance).

fall_velocity(earth, Distance) when Distance >= 0  -> math:sqrt(2 * 9.8 * Distance);
fall_velocity(moon, Distance) when Distance >= 0 -> math:sqrt(2 * 1.6 * Distance);
fall_velocity(mars, Distance) when Distance >= 0 -> math:sqrt(2 * 3.71 * Distance).

The -export directive makes only fall_velocity/1, the tuple version, public. The fall_velocity/2 function is available within the module, however. It’s not especially necessary here, but this “make one version public, keep another version with different arity private” is common in situations where you want to make a function accessible but don’t necessarily want its inner workings directly available.

If you call this function—the tuple version, so curly braces are necessary—fall_velocity/1 calls the private fall_velocity/2, which returns the proper value to fall_velocity/1, which will return it to you. The results should look familiar.

1> c(drop).
{ok,drop}
2> drop:fall_velocity({earth,20}).
19.79898987322333
3> drop:fall_velocity({moon,20}).
8.0
4> drop:fall_velocity({mars,20}).
12.181953866272849

There are a few different ways to extract the data from the tuple. You could reference the components of the tuple by number using the built-in function element, which takes a numeric position and a tuple as its arguments. The first component of a tuple can be reached at position 1, the second at position 2, and so on.

fall_velocity(Where) -> fall_velocity(element(1,Where) , element(2,Where)).

You could also break things up a bit and do pattern matching after getting the variable:

fall_velocity(Where) ->
   {Planemo, Distance} = Where,
  fall_velocity(Planemo, Distance).

This function has more than one line. Note that actions are separated with commas, and that only the last line ends with a period. The result of that last line will be the value the function returns.

The pattern matching is a little different. The function accepted a tuple as its argument and assigned it to the variable Where. (If Where is not a tuple, the function will fail with an error.) Extracting the contents of that tuple, since we know its structure, can be done with a pattern match inside the function. The Planemo and Distance variables will be bound to the values contained in the Where tuple, and can then be used in the call to fall_velocity/2.

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

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