Chapter 20. Style and Efficiency

Throughout this book, we have covered the do and don’ts of Erlang programming. We have introduced good practices and efficient constructs while pointing out bad practices, inefficiencies, and bottlenecks. Some of these guidelines you will probably recognize as being relevant to computing in general; others will be Erlang-related, and some will be virtual-machine-dependent. Learning to write efficient and elegant Erlang code will not happen overnight. In this chapter, we summarize design guidelines and programming strategies to use when developing Erlang systems. We cover common mistakes and inefficiencies and look at memory handling and profiling.

Applications and Modules

A collection of tightly interacting modules in Erlang is called an application. Erlang systems consist of a set of loosely coupled applications. It is good practice to design an application to provide a single point of entry for calls originating from other applications. Collecting all externally exported functions into one module provides flexibility in maintaining the code base, since modifications to all “internal” modules are not visible to external users. The documentation for this module gives a complete description of the interface to the application. This single point of entry also facilitates tracing and debugging of the call flow into the application. In large systems, it is good practice to prefix the modules in a particular application with a short acronym. This ensures that the choices of module names in different applications will never overlap, which would cause problems if both applications were used together in any larger system.

Modules are your basic building blocks. When designing your modules, you should try to export as few functions as possible. As far as the user of a module is concerned, the complexity of a module is proportional to the number of exported functions, since the user of a module needs to understand only the exported functions. Having as small an interface as possible also gives the maintainer of the application greater flexibility, as it makes it much easier to refactor the internal code.

You should try to reduce intermodule dependencies as much as you can. It is harder to maintain a module with calls to many modules instead of calls to just a few modules. Reducing interdependencies not only facilitates the maintenance of the modules being called, but also makes it easier to refactor them as a smaller number of external calls have to be maintained.

Intermodule dependencies should form an acyclic graph, as shown in Figure 20-1; that is, there should be no module X, say, that depends on another module that (through a chain of dependencies) depends on module X.

Intermodule dependencies
Figure 20-1. Intermodule dependencies

Within a particular module, place related functions close to each other.[48] For example, you should place functions such as start and stop and init and terminate next to each other, and place all of the message-handling functions in the same part of a module. Keeping related functions close together makes your code easier to follow and inspect, especially for cases where you do or open something in one function and undo or close it in another.

Libraries

You should put commonly used code into libraries. Libraries should be collections of related functions, possibly those that manipulate values of the same type. Where possible, ensure that library functions are free of side effects, as doing so will enhance their reusability. When there are functions with side effects such as message passing, destructive database operations, or I/O, you should try to ensure that all the operations with side effects that are related—for instance, all those manipulating a particular ETS table—are contained in a single library module.

Dirty Code

Dirty code consists of anything you “should not” do but are forced to do anyhow. Getting away from writing dirty code can sometimes be hard and other times impossible.

Dirty code includes the use of the process dictionary, the use of the process_info/2 BIFs or code that makes assumptions about the internal structure of data types, or other internal constructs. For example, you could use the process_info/2 BIF to view the length of the message queue or to peek at the first message in the mailbox. You might want to store a global variable in the process dictionary, or have a dynamic function to look up a record type and generically use it to forward a request to a callback module.

It is important to isolate tricky or dirty code into separate modules, and do everything possible to minimize and avoid such code. It is also crucial to document dirty code, and to say in what way it is dirty so that anyone modifying your code has a clear view of the assumptions you made when writing this dirty module.

Interfaces

Document all your exported interfaces. Documentation should include parameter values, possible ranges, and return values. If your function has side effects such as message passing, database updates, or I/O, you should include information on these. Provide references to specifications and protocols and document all principal data structures. Comments should be clear, informative, and not superfluous; you want to describe what the function does, not how it does it. An example of this is provided by the EDoc documentation in Chapter 18, a full version of which is available on this book’s website.

Decide why a function is exported. It is a good practice to divide export directives into categories based on the following function types:

  • A user interface function

  • An intermodule function (used in the same application, but not exported to others)

  • An internal export (used by the same module in BIFs such as apply/3 and spawn/3)

  • A standard behavior callback function (init, handle_call, etc.)

It obviously depends on how many functions you are exporting; a rule of thumb is that if your export clause spans more than one line, it is time to break it up. You might include client functions such as start/0, stop/0, read/1, and write/2 in one export directive, and callback functions such as init/1, terminate/2, and handle_call/3 in another. This allows anyone who is reading the code instead of the documentation to distinguish between exported interfaces and internal exports or callback functions.

Use the –compile(export_all) directive only when developing your code (and even then, only if you must). When the development work is done, don’t forget to remove the directive; more often than not, this does not happen.[49] An alternative to including this as a module directive is to make it an option on compilation, like so:

compile:file(foo, [compile_all,...]).

Return Values

If a function might not always succeed, tag its return values, because if you don’t a positive result might be interpreted as a negative one. In the following example, what happens if you do a key search on key 1 using the list [{0,true}, {1,false}, {2,false}]? How do you distinguish the return value of the key with the atom false returned as the base case when the entry is not found?

keysearch(Key, [{Key, Value}|_]) -> Value;
keysearch(Key, [_|Tail])         -> keysearch(Key, Tail);
keysearch(_key, [])              -> false.

The Erlang approach is to use standard return values of the form ok, {ok, Result}, or {error, Reason}. In our case, we would distinguish between a successful lookup using {ok, false} and a failed lookup using the atom false. The correct implementation of keysearch/2 would thus be:

keysearch(Key, [{Key, Value}|_]) -> {ok, Value};
keysearch(Key, [_|Tail])         -> keysearch(Key, Tail);
keysearch(_key, [])              -> false.

An alternative approach to this is to raise an exception in the case of no corresponding key value being found. With this approach, each use of keysearch will need to be in the context of a try ... catch construct to handle the exceptional case.

If you know that the function will always be successful, return a single value such as true or false, or just an integer, atom, or composite data type. This will allow the return value of this function to be passed as a parameter to another function without having to check for success or failure.

You should always pick return values that will simplify the caller’s task. Doing so will make the code more compact and readable. For example, if you know that get_status/1 will always succeed, why write a tagged return value, from which you have to extract the return value:

{ok, Status} = get_status(BladeId),
NewStatus = reset(BladeId, Status)

when all you need to do is return the Status? Doing so will allow the function call to be passed as an argument to the reset/2 call:

NewStatus = reset(BladeId, get_status(BladeId))

Make no assumptions about what the caller will do with the result of a function. In the following example, we are assuming that the caller of the function wants to print out an error message stating that the person for whom we want to raise taxes is not Swedish:

tax_to_death(Person) ->
  case is_swede(Person) of
    true ->
       {ok, raise_taxes(Person)};
    {error, Nationality} ->
       io:format("Person not Swedish:~p~n",[Nationality]),
       error
   end.

Let’s not make that assumption, and instead allow the caller of this function to make the decision based on the return value of the call:

tax_to_death(Person) ->
  case is_swede(Person) of
    true ->
      {ok, raise_taxes(Person)};
    {error, Nationality} ->
      {error, Nationality}
  end.

Another way to look at this example is that, in general, it is best for a function to do one thing: in this case, perform the update or print an error message of a particular form. If you find you have a function doing two things, you can refactor it into two separate functions, each of which can be reused separately.

Internal Data Structures

Do not allow private data to leak out. All details of private data structures should be abstracted out of the interface. In abstracting your interfaces, encapsulate information that the users of the function do not need. Design with flexibility so that any changes introduced to your internal data representations will not influence the exported functional interface. The following queue module:

-module(q).
-export([add/2, fetch/1]).

add(Item, Q) -> lists:append(Q, [Item]).

fetch([H|T]) -> {ok, H, T};
fetch([])    -> {error, empty}.

could be used as follows:

NewQ = [],
Queue1 = q:add(joe, NewQ),
Queue2 = q:add(klacke, Queue1).

In the preceding code, we are leaking out the fact that the queue data structure is a list when we bind the variable NewQ to the empty queue. This is not good. The q module should have exported the function empty() -> [], which we use to create the empty queue we bind to the variable NewQ:

Newq = q:empty(),
Queue1 = q:add(joe, Newq), ...

Now, it would be possible to rewrite the queue implementation, for instance, as a pair of lists keeping the front and rear separately, without having to rewrite any code using the queue module. One exception to this is if you want to determine whether two queues are equal; in this case, you will need to define an equality function over queue representations, because the same queue can be represented in different ways in this new implementation.

Processes and Concurrency

Processes are the basic structuring elements of a system in Erlang. A fundamental principle of design in Erlang is to create a one-to-one mapping between the parallel processes in your Erlang program and the set of parallel activities in the system you are modeling. This could add up to quite a few simultaneous processes, so where possible, avoid unnecessary process interaction and unnecessary concurrency. Ensure that you have a process for every concurrent activity, not for only two or three! If you are coming from an object-oriented background, this will not equate to a process for every object or every method applied to an object. Or, if you are dealing with users logged in to a system, this will probably not equate to a process for every session. Instead, you will have a process for every event entering the system. This will equate to massive numbers of transient processes and a very limited number of persistent ones.

You should always implement a process loop and its peripheral functions in one module. Avoid placing code for more than one process loop in the same module, as it becomes very confusing when you try to understand who is executing what. You might have many process instances executing the same code base, but ensure that the loop in the module is unique.

Hide all message passing in a functional interface for greater flexibility and information hiding. Instead of having the following expression in your client code:

resource_server ! {free, Resource}

replace it with this function call:

resource_server:free(Resource)

Place the client functions in the same module as the process. Client functions are functions called by other processes that result in a message being sent to the process defined in the module. This makes it easy to follow the message flow without having to jump between modules, let alone having to find out in which module the clause receiving the messages is located. From your end, it reduces and simplifies the documentation you have to produce, as you need to describe only the functional API, and not the message flow. You will also get more flexible code, as you are hiding the following:

  • That the resource server is a process

  • That it is registered with the alias resource_server

  • The message protocol between the client and the server

  • The fact that the call is asynchronous

If you use a functional interface, you have the flexibility to change all of this without affecting the client code.

Registered processes should, where possible, have the same name as the module in which they are implemented. This facilitates the troubleshooting of live systems and improves code readability and maintainability. Registered processes should have a long life span; you should never toggle between registering and deregistering them. As the space used by atoms is not garbage-collected, avoid dynamically creating atoms to register processes, as this might result in a memory leak.

Processes should have well-defined behaviors and roles in the system. You should seriously consider using the OTP behaviors described in Chapter 12, including servers, event handlers, finite state machines, supervisors, and applications.

When working with message passing, all messages should be tagged. It makes the order of clauses in the receive statement unimportant, and as a result, it facilitates the addition of new messages without changing the existing behavior, thus reducing the risk of bugs.

Avoid pattern matching only on unbound variables in receive clauses. In the following example, what if you want your process to also handle the message {get, Pid, Variable}?

loop(State) ->
  receive
    {Mod, Fun, Args} ->
      NewState = apply(Mod, Func, Args),
      loop(NewState)
  end.

It is easy to see that you would have to match the message above the {Mod, Fun, Args} pattern, but what if your receive statement was matching on many more messages? It would not be that obvious. Or, what would happen if you want to apply a function in the module get? By tagging messages, you get full flexibility in rearranging the order of your messages and avoid incorrectly pattern matching messages in the wrong clause:

loop(State) ->
  receive
    {apply, Mod, Fun, Args} ->
      NewState = apply(Mod, Func, Args),
      loop(NewState);
    {get, Pid, Variable} ->
      Pid ! get_variable(Variable),
      loop(State)
  end.

When using receive clauses, you should not be receiving unknown messages. If you do, they should be treated as bugs and you should allow your system to crash, just as an OTP application will do. If you don’t let the system crash, make sure you log the unexpected messages. This allows you to ascertain where they are coming from, and to diagnose how to deal with them. You will probably find that these messages originate from ports or sockets or from bugs that should be picked up during a unit test.

If you do not handle unknown messages, you will notice that the CPU usage of your system will start to increase and that the response time will decrease, until the Erlang runtime system runs out of memory and crashes. The increase in CPU usage and response time is explained by the fact that whenever a process receives a message, it has to traverse the potentially hundreds or thousands of messages in the mailbox before reaching one that matches. The more messages that are not matched, the more memory is leaked.

When you are sending and receiving messages, you should hide the internal message protocol from the client. Always tag the messages using the client function name, as it facilitates the hop from the client function to the location in your process loop where you handle the message:

free(Resource) ->
   resource_server ! {free, Resource}.

Use references. In complex systems where similar responses and requests might originate from different processes, use references to uniquely identify responses from specific requests:

call(Message) ->
  Ref = make_ref(),
  resource ! {request, {Ref, self()}, Message},
  receive
    {reply, Ref, Reply} -> Reply
  end.

reply({Ref, Pid}, Message) ->
  Pid ! {reply, Ref, Message}.

Be careful with timeouts. If you use them, always flush messages that arrive late. If you don’t, your next request will result in the response that previously timed out. You can avoid this problem by using references and assuming that no other processes will send messages of the format {reply, Ref, Reply}. Just flush messages you do not recognize:

call(Message) ->
  Ref = make_ref(),
  resource ! {request, {Ref, self()}, Message},
  wait_reply(Ref).

wait_reply(Ref) ->
  receive
    {reply, Ref, Reply} -> Reply;
    {reply, _,   Reply} -> wait_reply(Ref)
  end.

If you are using timeouts to deal with the case when a process might have terminated, it is much better to use a link or a monitor.

Be very restrictive in trapping exits, and when doing so, do not toggle. Make sure you remember to flush exit signals, and when linking and unlinking processes, be aware of race conditions. When you link to a process, it might have already crashed. When you unlink, it might have terminated, and its EXIT signal will be waiting to be retrieved from the process mailbox.

Separate error recovery and normal code, as combining them will increase the complexity of your code. It will also ensure that crashes and recovery strategies are handled consistently. Never try to fix an error that should not have occurred and then continue. If an unexpected error occurs, make your process crash and let a supervisor process deal with it. It is more likely that in trying to handle errors, you will generate more bugs than you think you are solving. In the following example, what would you do if the variable List is not a list?

bump(List) when is_list(List) ->
  lists:map(fun(X) -> X+1 end, List);
bump(_) ->
  {error, no_list}.

Everywhere you call bump/1, you would have to cater for two return values in a case clause, but still not make the person reading the code any wiser as to why List got corrupted. Don’t be defensive, and instead write:

bump(List) ->
  lists:map(fun(X) -> X+1 end, List).

If List does not contain a list, a runtime error will occur in lists:map/2 and will result in your process terminating. Let your supervisor handle the exit signal and let it decide the recovery strategy consistently with all of the other processes it is supervising. Make sure the crash is recorded and can be used for post-mortem debugging; log these errors and crashes in a separate error logger process.

As we should be hiding processes and message passing behind functional interfaces, ensuring that the dependencies form an acyclic graph also ensures that there are no deadlocks between these processes. Deadlocks are extremely rare in Erlang, but they do occur if the concurrency is not well thought out and properly designed. Ensuring that there are no cycles in your module dependencies is a step in the right direction.

Stylistic Conventions

Programs are written not just to be executed by a computer, but also to be read and understood by their authors and other programmers. Writing your programs in a way that makes them easier to read and understand will help you remember what your program does when you come back to it six months down the road, or will help another programmer who has to use or modify your program. It will also make it easier for someone to spot errors or other problems, or to interpret debugging information. So, being consistent in the way that you write programs will help everyone. In this section, we give a set of commonly used conventions for style in writing Erlang programs.

First, avoid writing deeply nested code.[50] In your case, if, receive, and fun clauses, you should never have more than two levels of nesting in your code. Here is how not to do it:

reset(BladeId, AdminState, OperState) ->
  case AdminState of
    enabled ->
      case OperState of
        disabled ->
          enable(BladeId);
        enabled ->
          disable(BladeId),
          enable(BladeId)
      end;
    disabled ->
      {error, admin_disabled}
  end.

A common trick to reduce indentation is to create temporary composite data types. If you have nested if and case statements, join them together in a tuple and pattern-match them in one clause:

reset(BladeId, AdminState, OperState) ->
  case {AdminState, OperState} of
    {enabled, disabled} ->
      enable(BladeId);
    {enabled, enabled} ->
      disable(BladeId),
      enable(BladeId);
    {disabled, _OperState} ->
      {error, admin_disabled}
  end.

You can reduce indentation by introducing pattern matching in your function heads. By pattern matching on the AdminState and the OperState in the function clause, not only do we make the code more readable, but we also reduce the level of nested clauses to zero and reduce the overall code size:

reset(BladeId, enabled, disabled) ->
  enable(BladeId);
reset(BladeId, enabled, enabled) ->
  disable(BladeId),
  enable(BladeId);
reset(_BladeId, disabled, _OperState) ->
  {error, admin_disabled}.

Avoid using if clauses when case clauses are a better fit. This is especially common with programmers coming from an imperative background who still do not feel at ease with pattern matching. Always ask yourself whether you can rewrite your if clause into a case clause using pattern matching and guards that will avoid pattern matching on the atoms true and false. If so, is it more readable and compact?

get_status(A,B,C) ->
  if
    A == enabled ->
      if
        B == enabled ->
          if
            C == enabled ->
              enabled;
            true ->
              disabled
          end;
        true ->
          disabled
      end;
    true ->
      disabled
  end.

The preceding example is an extreme occurrence of the misuse of the if statement. By creating a composite data type with the variables A, B, and C, and replacing the if with a case statement, we reduce the level of indentation and make the code more readable and compact:

get_status(A,B,C) ->
  case {A,B,C} of
    {enabled,  enabled,   enabled } -> enabled;
    {_status1, _status2, _status}   -> disabled
  end.

Aim to keep your modules to a manageable size. Short modules facilitate maintenance and debugging as well as the understanding of your code. A manageable module should have no more than 400 lines of code, comments excluded. Split long modules in a logically coherent way and remember that long lines will not solve your problem.

Lines of code should not be too long, or drift across the page. A line of code should never be more than 80 characters long. Too many times developers have tried to convince us that their long lines were readable by widening their editor window to cover the whole screen. That will not solve your problem.

If your code is drifting across the page, spending a few minutes on reformatting will save the next person from spending hours trying to maintain and debug it. The following code style is a typical example of what happens when someone writes code, tests it, but never goes back to review it:

name(First, Second) ->
  case person_exists(First, Second) of
    true ->
      Y = atom_to_list(First) ++ [$ |atom_to_list(Second)] ++
                                          [$ |get_nickname(First,
                                             Second)],
      io:format("true person:~s~n",[Y]);
    false ->
           ok
  end.

You can easily rewrite this to the following:

name(First, Second) ->
  case person_exists(First, Second) of
    true ->
      Y = atom_to_list(First) ++
          [$ |atom_to_list(Second)] ++
          [$ |get_nickname(First, Second)],
      io:format("true person:~s~n",[Y]);
    false ->
      ok
  end.

After a second iteration and a bit of thinking, the preceding code would convert to the following:

name(First, Second) ->
  case person_exists(First, Second) of
    true ->
      NickName = get_nickname(First, Second),
      io:format("true person:~w ~w ~s~n",[First, Second, NickName]);
    false ->
      ok
  end.

If your code drifts across the page, solve the problem by:

  • Picking shorter variable and function names

  • Using temporary variables to divide your statement or expression across several lines of code

  • Reducing the level of indentation in your case, if, and receive statements

  • Moving possibly duplicated code into separate functions

Choose meaningful function and variable names. If the names consist of several words, separate them with either capital letters or an underscore. Some people will pick one style for variables and the other for functions, without mixing them together. Whatever the case, when you pick a style, stick to it throughout your code; this is true of all the conventions discussed here: consistency makes code readable, whereas chopping and changing between styles will distract a reader away from understanding what is going on.

Avoid long names, as they are a prime reason for code drifting across a page. A long name might look all right in the function head, but imagine calling it in the second level of a clause together with long variable names. If you are using longer names, it makes more sense to use them in functions—which will potentially be used across your code base—rather than for variables whose scope will be limited to a particular function.

Use abbreviations and acronyms to shorten your names, but ensure that they make sense and are easy to understand. A common pitfall is to use names that are very similar, and so can easily be confused: Name, Names, and Named all look pretty similar! If you use prefixes, pick ones that will provide hints regarding the variable types or function return values. Finding meaningful function and variable names takes practice and patience, so don’t underestimate the task.

Avoid the underscore on its own, when using “don’t care” variables. Even if you are not using the variable, its contents might be of interest to whoever is reading the code. Not using an underscore in front of unused variables will result in compiler warnings. But be warned that variables of the format _name are regular single-assignment variables, so once they have been bound, they cannot be reused as “don’t care” variables.

In the following example, if the variable AdminState is bound to the atom disabled, _State will also be bound to disabled. If OperState is bound to enabled, the second case clause will fail with a case clause error, as none of the patterns match:

restart(BladeId, AdminState, OperState) ->
  case AdminState of
    enabled ->
      disable(BladeId);
    _State ->
      ok
  end,
  case OperState of
    disabled ->
      ok;
    _State ->
      stop(BladeId)
  end.

Use records as the principal data structure to store compound data types. If records are used by one module only and are not exported, the definition should be placed at the beginning of the module to stop others from using it in their modules. Records used by many modules should be placed in an include file and properly documented.

Use record selectors to access a field and pattern matching to access values in more than one field:

Cat = #cat{name = "tobby", owner ="lelle"},
#cat{name = Name, owner = Owner} = Cat,
Name2 = Cat#cat.name.

Never, ever use the internal tuple representation of records, as this defies the flexibility that records bring to the table:

Cat = #cat{name = "tobby", owner = "lelle"},
{cat, Name, _owner} = Cat

If you write code such as this and then add a field to your record, you will have to update the code that uses the tuple representation even if the function is not affected by the field.

Use variables only when you need them. There are two reasons for using a variable. The first is when you need to use the value they are bound to more than once. The second is to improve clarity in your code and shorten the length of a line. You might be tempted to write the following:

Sin = sin(X),
Cos = cos(X),
Tan = Sin / Cos

But passing the return values of the function directly to another function will make your code much clearer and more compact:

Tan = sin(X) / cos(X)

Always ask yourself whether the variable really makes the code clearer; the more you program in Erlang, the less you will find yourself using variables.

Be careful when using catch and throw. It is nearly always preferable to use the try ... catch construct than to use catch and throw. Try to minimize their usage, and ensure that any throw is caught in the same module as a catch. As you saw in Chapter 3, it makes sense to use catch only when you can safely ignore the return value of a function.

If you are using catch and throw, always make sure you also handle runtime errors that might be caught. For instance, the following code appeared in code that was about to be sent into production:

Value = (catch get_value(Key)),
ets:insert(myTable, {Key, Value})

What would have happened if a runtime error occurred in the get_value/1 call? You would have caught the runtime error {'EXIT', Reason}, bound it to the variable Value, and stored it in the ETS table.

Be very careful with the process dictionary; in fact, avoid it like the plague. The BIFs put and get are destructive operations. They will make your functions nondeterministic and hard to debug, as determining the contents of the process dictionary after the process has crashed will be very difficult, if at all possible, to do. Instead of the process dictionary, introduce new arguments in your functions.

If you do not know what the process dictionary is,[51] move along; there is nothing to see here, move along.

Use the -import(mod, [fun/arity...]) directive with care. It can make the code hard to follow and will initially confuse the most experienced of Erlang programmers. A temptation might be to use it when reducing the length of lines, but this comes at the expense of comprehension. There are some cases where it makes sense:

  • Common functions from the lists module, such as map, foldl, and reverse, are going to be comprehensible to anyone reading your code.

  • If you want to move EUnit tests from the module foo to the module foo_tests without changing them, you will need to import all the definitions from foo into foo_tests.

Developing your own Erlang programming style might take years. Make sure you are consistent with yourself in any single system. This includes your use of indentation and spaces as well as your choice of variable, function, and module names. In large projects, style guidelines are often given to you.

Coding Strategies

The user should be able to predict what will happen when using the system. The foundation of this determinism originates in predictable and consistent results being returned by your functions. A consistent system in which modules do similar things is easier to understand and maintain than a system in which modules do similar things in quite different ways.

Minimize the number of functions with side effects, collecting them in specific modules. These modules could handle files or encapsulate database operations. Functions with side effects will cause permanent changes in system state. Knowledge of these states is imperative for these functions to be used and debugged, making reusability and maintainability of the code difficult. Try to reduce the number of side effects by maximizing the number of pure functions, separating side effects into atomic functions rather than combining them with functional transformations.

Some of the most challenging bugs are those caused by race conditions, especially in the advent of multicore systems in which code is truly running in parallel. Here’s a common scenario: you run a test and are able to re-create the same bug every time. As soon as you turn on trace printouts, the overhead causes the process to run more slowly, changing the order of execution. All of a sudden, your bug is not reproducible anymore. What can you do to avoid this? Make your code as deterministic as possible.

A deterministic program is one that will always run in the same manner and provide the same result or manifest the same bug, regardless of the order of execution. What makes a solution deterministic? Assume a supervisor has to start five workers in parallel and that the start order of these processes does not matter. A nondeterministic solution would spawn all of them and check that they have started correctly. A deterministic solution would spawn the processes one at a time, ensuring that each one has started correctly before proceeding to the next one. Although both might provide the same result, the nondeterministic solution may make start errors hard to reproduce, providing different results based on the order in which the processes have been spawned. Although determinism is not guaranteed to eliminate race conditions, it will certainly reduce the number of them and will make your system more predictable, as well as making debugging much more straightforward.

Abstract out common design patterns. If you have the same code in two places, isolate it in a common function.[52] If you have similar patterns of code, try to define the difference in terms of a variable or a separate function, combining the pattern in a common call. Where appropriate, replace your recursive functions iterating on lists with a call to one of the higher-order functions in the lists module. You can encapsulate your functionality on the list elements in a fun and choose a recursive pattern using one of the higher-order functions provided in the lists module. To see what is happening to the elements in the list, all you need to do is inspect the fun. To see what recursive pattern is being applied, examine which function in the lists module is being called.

When you are designing a library of your own, you will find that many of the functions follow similar patterns, such as fold or map on lists. You can then write your own higher-order functions to encapsulate these patterns, allowing you and other users of the module to abstract their functions. With higher-order functions encapsulating common computation patterns, many recursive functions with multiple clauses and base cases become much more compact, readable, and maintainable.

Avoid defensive programming. Trust your input data if it originates from within the system, testing it only if it enters through an external interface. Once the data has entered the runtime system, it is the responsibility of the calling function to ensure that the input data is correct, and not of the function that was called. If your function is called with erroneous input data, the advice is to make it crash.

An example of defensive programming would be a function converting atoms denoting months to their respective numbers:

month('January') -> 1;
month('February') -> 2;
...
month('December') -> 12;
month(_other) -> {error, badmonth}.

It might be tempting to add the last clause acting as a catchall. If month/1 is called with an incorrect atom, it would return the tuple {error, badmonth}. This return value would either force the user of the function to test for and handle this error value, or would cause a runtime error somewhere in the system, where a function expecting an integer receives the tuple. Removing the last defensive clause would instead cause a function clause error, providing the call chain and the misspelled atom in the crash report.

Do not future-proof your code. Don’t try to write code that will be able to deal with every possible eventuality as the system evolves. It will make your code harder to understand and maintain, adding unnecessary complexity. And best of all, you will probably not end up using what you have added anyhow. Be kind to Erlang consultants supporting and maintaining your code in the years to come. Do not try to predict what will happen to your code after it has gone into production. Just implement what is needed.

Here are two final pieces of advice that are not necessarily related to Erlang, but to programming in general: avoid cut-and-paste programming and do not comment out dead code, just delete it, as you can always get it back from your repository if you need to sometime in the future.[53]

Efficiency

The Erlang virtual machine is constantly being optimized and improved. What might have been inefficient constructs or necessary workarounds in earlier releases are not necessarily a problem in the current version. So, beware when reading about efficiency, workarounds, and optimizations in old performance guides, blog entries, and especially old posts in newsgroups and mailing list archives. If in doubt, always refer to the release notes and documentation of the runtime system you are using. And most importantly, benchmark, stress test, and profile your systems accordingly.

Sequential Programming

The most common misconceptions regarding efficiency concern funs and list comprehensions. List comprehensions allow you to generate lists and filter elements, and funs allow you to bind a functional argument to a variable. Today, the compiler translates list comprehensions to ordinary recursive functions, and funs were optimized a long time ago and have gone from being highly inefficient black magic to having performance between that of a regular function call and using an apply/3.

Strings are not implemented efficiently in Erlang. In the 32-bit representation, every character consists of four bytes, with an additional four bytes pointing to the next character. In the 64-bit representation, this doubles to eight bytes. On the positive side, Unicode is not an issue. On the negative side, if you are dealing with large data sets and memory does become an issue, you will need to convert your strings to binaries and match using the bit syntax.

Use the re library module to handle regular expressions if speed and throughput are important. This library supports PCRE-style regular expressions in Erlang, with a substantially more efficient implementation than the regexp library module, the use of which is now deprecated.

At one time, you were encouraged to reorder your function clauses as well as receive and case clauses, putting the most common patterns at the top. Today, the compiler will rearrange the clauses for you, and in most cases will use an efficient binary search to jump to the right clause, regardless of the number of clauses. An exception is if you have code of the following form:

month('January') -> 1;
month('February') -> 2;
month(String) when is_list(String) -> {error, badmonth};
month('March') -> 3;
...
month('December') -> 12;
month(_other) -> {error, badmonth}.

As the variable String will always match and be bound, with the clause possibly failing on the guard, the compiler needs to treat this case separately. It will first try to match 'January' or 'February' using a binary search, after which it binds the argument to the variable String and evaluates the guard. If the guard fails, a new binary search is executed on the remaining months. Moving the guarded clause either to the beginning:

month(String) when is_list(String) -> {error, badmonth};
month('January') -> 1;
month('February') -> 2;
...

or as the next-to-last clause, immediately before month(_other) -> ..., will improve efficiency slightly.

If you are setting and resetting a lot of timers, avoid using the timer module, as it serializes all requests through a process and can become a bottleneck. Instead, try using one of the Erlang timer BIFs erlang:send_after/3, erlang:start_timer/2, erlang:cancel_timer/1, or erlang:read_timer/1.

Where possible, use tuples instead of lists. The size of the tuple is two words plus the size of each element. Lists will consume one word for every element. As a result, tuples consume less memory and are faster to access.

Keep in mind that atoms are not garbage-collected. If you generate atoms dynamically using the list_to_atom/1 BIF on dynamic data, you might eventually run out of memory or reach the limit of allowed atoms, which is slightly more than 1 million. This BIF, if converting external data to atoms, makes your system open for denial of service attacks. Where this is a possibility, you should instead be using list_to_existing_atom/1.

Lists

Always ask yourself whether you really need to work with flat lists, as the function lists:flatten/1 is an expensive operation. I/O operations through ports and sockets accept nonflat lists, including those consisting of binary chunks, so there is no need to flatten the list.

The same applies to the BIFs iolist_to_binary/1 and list_to_binary/1. If your list is of depth one, use lists:append/1 instead:

1> Str = [$h,[$e,[$l,$l],$o]].
[104,[101,"ll",111]]
2> io:format("~s~n",[Str]).
hello
ok
3> lists:append([[1,2,3],[4,5,6]]).
[1,2,3,4,5,6]

Left-associated concatenation is inefficient. Concatenating strings using the following:

lines(Str) ->
  "Hello " ++ Str ++ " World".

will result in the strings on the left side of the ++ being traversed multiple times. Instead of using ++, you can let the compiler do the concatenation for you by writing:

lines(Str) ->
  ["Hello ", Str, " World"].

Do not append elements to the end of a list using ++ through List ++ [Element] or lists:append(List, [Element]). Every time you append an element, the list on the lefthand side of the ++ needs to be traversed. Do it once and you might get away with it. Do it recursively, and then for every recursive iteration, you will start getting serious performance problems:

double([X|T], Buffer) ->
  double(T, Buffer ++ [X*2]);
double([], Buffer) ->
  Buffer.

It is much more efficient to add the element to the head of the list and when the recursive call reaches the base case, reverse it. This way, you traverse the list only twice:

double([X|T], Buffer) ->
  double(T, [X*2|Buffer]);
double([], Buffer) ->
  lists:reverse(Buffer).

If you are dealing with functions that accept nonflat lists, add your element to the end of the nonflat list by using [List, Element], creating a list of the format [[[[[1],2],3],4],5]. It will save you from having to reverse the list once you’re done.

So, you should use ++ when appending lists that you know do not consist of single elements where the result has to be flat. Isn’t the following line of code:

List1 ++ List2 ++ List3

more elegant than this?

lists:append([List1, List2, List3])

Try to traverse lists only once. With small lists, you might not notice any difference in execution time, but as soon as your code gets into production and the line length increases, performance might become an issue. In the following example, we take a list of integers, extract all of the even numbers, multiply them by a multiple, and add them all together:

even_multiple(List, Multiple) ->
  Even = lists:filter(fun(X) -> (X rem 2) == 0 end, List),
  Multiple = lists:map(fun(X) -> X * Multiple end, Even),
  lists:sum(Multiple).

We traverse the list three times in this definition of even_multiple. Instead, encapsulate the filtering, and add the integers in a fun, and use a higher-order function to traverse the list. This allows you to implement the same operation more efficiently, by traversing the list only once:

even_multiple(List, Multiple) ->
  Fun = fun (X, Sum) when (X rem 2) == 0 ->
              X + Sum;
            (X, Sum) ->
              Sum
        end,
  Multiple * lists:foldl(Fun, 0, List).

Tail Recursion and Non-tail Recursion

Non-tail-recursive functions are often more elegant than tail-recursive functions, but when dealing with large data sets, be careful of large bursts of memory usage. These bursts can occasionally result in your Erlang runtime system running out of memory and therefore terminating. Building a large data structure can be efficient in non-tail-recursive form, but other operations on large data structures, necessitating deeply recursive calls, may be more efficient using tail recursion. This is because it makes last-call optimization possible, and as a result, it will allow a function to execute in a constant amount of memory.

In the end, our advice about this is to make sure you measure the performance of your system to understand its memory usage and behavior.

Concurrency

Operations that require a lot of memory will affect performance, as they will trigger the garbage collector more often. When dealing with memory-intensive operations, spawn a separate process, terminating it once you’re done. The garbage collection time will be reduced as all its memory area will be deallocated at once.

You can take this one step further and spawn a process using the following:

spawn_opt(Module, Function, Args, OptionList)

where OptionList includes the tuple {min_heap_size, Size}. Size is an integer denoting the size of the heap (in words) allocated when spawning the process. The default allocated heap size is 233 words, a conservative value set to allow massive concurrency.

When fine-tuning your system, increasing the heap size will reduce the number of garbage collections, potentially speeding up some operations. Use min_heap_size with care and make sure to measure your system performance before and after ensuring that the change you made has had the desired effect. If you are not careful, your system might end up using more memory than necessary and run more slowly due to the worsened data locality. You can also set the heap size for all processes when starting the Erlang runtime system by setting the +h Size flag to erl.

Tune for full garbage collection. If you remember the discussion in Chapter 3, the memory heap is divided into the new heap and the old heap. Data in the new heap that survives a sweep by the garbage collector is moved to the old heap. The option {fullsweep_after, Number} makes it possible to specify the number of garbage collections which should occur before the old heap is swept, regardless of whether the old heap is full.

Setting the value to 0 will force a full sweep every time. This is a useful option in embedded systems where memory is limited. If you are using lots of short-lived data, especially large binaries, see whether setting the value between 10 and 20 will make a difference. Use the fullsweep_after option only if you know there are problems with the memory consumption of your process, and ensure that it makes a difference. You can set the fullsweep_after and min_heap_size flag options globally for all newly spawned processes using the erlang:system_flag(Flag, Value) BIF.

When a process is suspended in a receive clause waiting for an event, the garbage collector will not be triggered unless a message is received and more memory is required. This is regardless of how long the process waits or of the quantity of unused data in the heaps. To get around this, you can force a garbage collection using the BIF garbage_collect/0 on the calling process or garbage_collect/1 on a particular one. You can save even more memory by using erlang:hibernate/3; use of this is supported in gen_server, gen_fsm, and gen_event.

Use binaries to encode large messages. Messages sent between processes result in the message being copied from the stack of the sending process to the heap of the receiving one. Avoid unnecessary concurrency, and, where possible, keep messages small.

If you need to send a large message to many processes, convert it to a binary. Binaries larger than 64 bytes (the reference counted binaries) are passed around as pointers, stopping large amounts of data from being copied among processes. So, if you have many recipients or are forwarding the unchanged message to many processes, the cost of converting your data type to a binary will be less than copying it from the stack of the sending process to the heap of every receiving process.

It is also more efficient to use binaries when sending large amounts of data to ports and sockets. In particular, there is no need to convert your output to lists of integers.

And Finally...

Let others review your code, as they will provide you with feedback and comments on style and optimizations. Always try to write clean and understandable code; type-specify your interfaces and model your data structures. Put effort in choosing algorithms and constructs that scale effectively. You have to think about real-time efficiency from the start, as it is not something that you can easily resolve later. And finally, never optimize your code in your first stage of development; instead, always program with maintainability and readability in mind. When you have completed your application, profile it and optimize your code only where necessary.

Common mistakes often made by beginners include:

  • Functions that span many pages

  • Deeply nested if, case, and receive statements

  • Badly typed and untagged return values

  • Badly chosen function and variable names

  • Unnecessary or superfluous processes

  • Badly and unindented code

  • Use of put and get

  • Misuse of catch and throw

  • Bad, superfluous, or missing comments

  • Usage of tuple representation of records

  • Bad and insufficient use of pattern matching

  • Trying to make the program fast, with unnecessary optimizations

A programmer will be able to understand a problem in full only when he has solved it at least once. And when he has found a solution, he will have thought of much better ways he could have solved the problem. Always try to go back and rewrite your code, keeping the aforementioned beginner errors in mind. You will quickly discover that your refactored program will consist of an elegant and efficient code base which, as a result, becomes easier to test, debug, and maintain.[54] Expect code reductions of up to 50% when rewriting your first major Erlang programs. As you become more experienced and develop your programming style, this reduction will decrease as you start getting things optimal the first time around.

The golden rules when working with Erlang should always be as follows:

First make it work.

Then make it beautiful.

And finally, only if you really have to, make it fast while keeping it beautiful.

You will quickly discover that in the majority of cases, your code will be fast enough. Happy Erlang programming!



[48] Research in Chris Ryder’s Ph.D. thesis, “Software Measurement for Functional Programming,” University of Kent, 2004, suggests that this is, in fact, what programmers do as a matter of course.

[49] Perhaps even in this book!

[50] The Wrangler refactoring tool will indicate this and a number of other bad smells in Erlang code. Wrangler is available from http://www.cs.kent.ac.uk/projects/protest/.

[51] We barely mention it in this book for this reason.

[52] The Wrangler system can help you to find duplicate code and to factor it into a common function.

[53] Assuming, of course, that you are using version control....

[54] If you are writing industrial applications, don’t believe for one second that you will be the last one to touch your code. Be kind to the others who will take over after you!

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

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