The LFE Style Guide






Adatped from multiple sources
by Duncan McGreggor











publisher logo

Published by Cowboys 'N' Beans Books

https://github.com/cnbbookshttp://cnbb.pub/info@cnbb.pub




First electronic edition published: 2013

Second electronic edition published: 2020




Portions © 1987-2020 Klas Eriksson, Mike Williams, Joe Armstrong

Portions © 1992, 1993 Peter Norvig

Portions © 1992, 1993 Kent Pitman

Portions © 2013-2020, Duncan McGreggor

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License

Creative Commons License




Cover art: Original version from 1968 DEC FOCAL Programming Manual with stylised PDP-8 as the dominant cover graphic. Adaptation of the original as a spoof and homage was made in 2015 by Duncan McGreggor.

Preface

The LFE Style Guide takes inspiration (and often times actual content) directly from key sources in the Lisp, Erlang, and even Clojure developer communities. These are as follows

Note, however, that these are not considered sacrosanct sources of ultimate truth; (and neither is this guide). Instead, they contain practices that we have either adopted as-is, modified to some extent, or simply rejected (e.g., due to prior conventions established in MACLISP and LMI Lisp, their inapplicability due to LFE's unique status as a Lisp and Erlang dialect, etc.).

In general we suggest following the LFE style as outlined here if you are creating a new project. If you are contributing to a project maintained by someonoe in the community, we recommend consistency: using the style adopted by that project (for any contributions to that project).

Above all, enjoy the parenthesis.

Introduction

What is good style?1

Good style in any language consists of code that is:

  • Understandable
  • Reusable
  • Extensible
  • Efficient
  • Easy to develop and debug

It also helps ensure correctness, robustness, and compatibility. Maxims of good style are:

  • Be explicit
  • Be specific
  • Be concise
  • Be consisten
  • Be helpful (anticipate the reader's needs)
  • Be conventional (don't be obscure)
  • Build abstractions at a usable level
  • Allow tools to interact (referential transparency)

Know the context when reading code:

  • Who wrote it and when?
  • What were the business needs?
  • What other factors contributed to the design decisions?

  1. This page was adatped from the Tutorial on Good Lisp Programming Style by Peter Norvig and Kent Pitman.

Formatting

Topics related to the manner of formatter LFE code.

File Headers

Every source file should begin with a brief description of the contents of that file.

After that description, every file should start the code itself with a (defmodule ...) form.

;;;; Variable length encoding for integers and floating point numbers.

(defmodule num-encode
  ...)

It is not necessary to include copyright info in every file as long as the project has a LICENSE file in its top-level directory. Files which differ in license from that file should get have a copyright notice in their header section.

If you are contributing to a project that has established a convention of adding copyright headers to all files, simply follow that convention.

Indentation

In general, use your text editor's indentation capabilities. If you are contributing to a particular library, be sure to ask the maintainers what standard they use, and follow those same guidelines, thus saving everyone from the drudgery of whitespace fixes later.

In particular, you'll want to do everything you can to follow the conventions laid out in the Emacs LFE mode supplied in the LFE source. Instructions for use are given in the LFE Github wiki, but we'll repeat it here. Simply edit your ~/.emacs file to include the following:

;; Prevent tabs being added:
(setq-default indent-tabs-mode nil)

;; LFE mode.
;; Set lfe-dir to point to where the lfe emacs files are.
(defvar lfe-dir (concat (getenv "HOME") "/git/lfe/emacs"))
(setq load-path (cons lfe-dir load-path))
(require 'lfe-start)

In general though, indentation is two lines per form, for instance:

(defun f ()
  (let ((x 1)
        (y 2))
    (lfe_io:format "X=~p, Y=~p~n"  (list x y))))

Note that LFE has many exceptions to this rule, given the complex forms it defines for features inherited from Erlang (e.g., pattern-matching in function heads). A few examples for the number exceptions to the two-space indentation rule above:

(cond ((lists:member x '(1 2 3)) "First three")
      ((=:= x 4) "Is four")
      ((>= x 5) "More than four")
      ('true "You chose poorly"))
(defun ackermann
  ((0 n)
   (+ n 1))
  ((m 0)
   (ackermann (- m 1) 1))
  ((m n)
   (ackermann (- m 1) (ackermann m (- n 1)))))

The last function would actually be better written as follows, but the form above demonstrates the indentation point:

(defun ackermann
  ((0 n) (+ n 1))
  ((m 0) (ackermann (- m 1) 1))
  ((m n) (ackermann (- m 1) (ackermann m (- n 1)))))

Maintain a consistent indentation style throughout a project.

Indent carefully to make the code easier to understand.

Use indentation to make complex function applications easier to read. When an application does not fit on one line or the function takes many arguments, consider inserting newlines between the arguments so that each one is on a separate line. However, do not insert newlines in a way that makes it hard to tell how many arguments the function takes or where an argument form starts and ends.

Bad:

(do-something first-argument second-argument (lambda (x)
    (frob x)) fourth-argument last-argument)

Better:

(do-something first-argument
              second-argument
              (lambda (x) (frob x))
              fourth-argument
              last-argument)

Vertical White Space

You should include one blank line between top-level forms, such as function definitions. Exceptionally, blank lines can be omitted between simple, closely related defining forms of the same kind, such as a group of related type declarations or constant definitions.

(defun +my-pi+ () 3.14)
(defun +my-e+ () 2.72)

(defun factorial (n)
  (factorial n 1))

(defun factorial
  ((0 acc) acc)
  ((n acc) (when (> n 0))
   (factorial (- n 1) (* n acc))))

Horizontal White Space

Do not include extra horizontal whitespace before or after parentheses or around symbols.

Furthermore, do not place right parentheses by themselves on a line. A set of consecutive trailing parentheses must appear on the same line.

Very bad:

( defun factorial
  (
    ( 0 acc)
    acc
  )
  (
    ( n acc)
      ( when ( > n 0)
  )
  ( factorial ( - n 1)
    ( * n acc
       )
     )
  )
)

Much better:

(defun factorial
  ((0 acc) acc)
  ((n acc) (when (> n 0))
   (factorial (- n 1) (* n acc))))

You should use only one space between forms.

You should not use spaces to vertically align forms in the middle of consecutive lines. An exception is made when the code possesses an important yet otherwise not visible symmetry that you want to emphasise.

Bad:

(let* ((low    1)
       (high   2)
       (sum    (+ (* low low) (* high high))))
  ...)

Better:

(let* ((low 1)
       (high 2)
       (sum (+ (* low low) (* high high))))
  ...))

You should align nested forms if they occur across more than one line.

Bad:

(defun munge (a b c)
(* (+ a b)
c))

Better:

(defun munge (a b c)
  (* (+ a b)
     c))

Line Length

You should format source code so that no line is longer than 80 characters.

Old text terminals were standardised on 80 columns which they in turn inherited from even older punch card technology. While modern computer screens support vastly more than this, there are a couple of considerations to keep in mind that motivate us to continue supporting an 80 character limit:

  • Displaying code in web pages, paste-bins, gist services, etc., is much cleaner and easier to read when the character width is limited to 80 characters.
  • Most modern text editors allow for multiple panes, allowing several files to be open side-by-side, supporting the easy editing and referencing of multiple files simultaneously; limiting these files to 80 characters in width facilitates this type of workflow.
  • Code that has to be examined under emergency circumstances (such as via a terminal attached to a crash cart in a data centre, or in an emergency shell session without a graphical window manager) is much easier to read quickly when character width is limited to 80.
  • Lastly, such a convention encourages good naming discipline!

Spelling and Abbreviations

Use correct spelling in your comments, and most importantly in your identifiers. The LFE documentation projects (books and reference materials) use aspell and include make targets for running various spell-checking tasks across the project files. Feel free to borrow from these for your own projects.

Use common and domain-specific abbreviations, and must be consistent with these abbreviations. You may abbreviate lexical variables of limited scope in order to avoid overly-long symbol names.

If you're not sure, consult a dictionary, look up alternative spellings in a dictionary, or ask a local expert.

Here are examples of choosing the correct spelling:

  • Use "complimentary" in the sense of a meal or beverage that is not paid for by the recipient, not "complementary".
  • Use "existent" and "nonexistent", not "existant". Use "existence", not "existance".
  • Use "hierarchy" not "heirarchy".
  • Use "precede" not "preceed".
  • Use "weird", not "wierd".

Make appropriate exceptions for industry standard nomenclature/jargon, including plain misspellings. For instance:

  • Use "referer", not "referrer", in the context of the HTTP protocol.

Naming

On names and naming in LFE code.

Symbols

Use lower case for all symbols (Erlang "atoms"). Consistently using lower case makes searching for symbol names easier and is more readable.

Place hyphens between all the words in a symbol. If you can't easily say an identifier out loud, it is probably badly named.

Always prefer - over / or . unless you have a well-documented overarching reason to, and approval from other hackers who review your proposal.

Bad:

(defun *default-username* ()"Ann")
(defun *max-widget-cnt* () 200)

Better:

(defun *default-user-name* () "Ann")
(defun *maximum-widget-count* () 200)

Unless the scope of a variable is very small, do not use overly short names like i and zq.

Names in Modules

When naming a symbol in a module, you should not include the module name as part of the name of the symbol. Naming a symbol this way makes it awkward to use from a client module accessing the symbol by qualifying it with a module prefix, where the module name then appears twice (once as part of the module name, another time as part of the symbol name itself).

Bad:

(defmodule varint
 (export
   (varint-length64 0))

(defun varint-length64 () ... )

(defmodule client-code)

(defun +padding+ ()
 (varint:varint-length64))

Better:

(defmodule varint
  (export
    (length64 0))

(defun length64 () ... )

(defmodule client-code)

(defun +padding+ ()
  (varint:length64))

Global Variables and Constants

Erlang, and thus LFE, does not support global variables or mutable data. However, many projects define constants in modules. Traditionally, Lisp projects have used symbols enclosed in + for global constants and symbols enclosed in * (a.k.a. "earmuffs") for global variables.

Adapted for LFE, one could use these conventions for module constants and default values, respectively.

(defun +my-pi+ () 3.14)
(defun *default-host* () "127.0.0.1")

Predicates

There are several options for naming boolean-valued functions and variables to indicate they are predicates:

  • a trailing ?
  • a trailing -p
  • a trailing p
  • a leading is-

Modern Lisps tend to prefer ?, while classic Lisps tend to use p. Erlang code tends to use is_ which translates to is- in LFE. you should use "P" when the rest of the function name is one word and "-P" when it is more than one word.

A rationale for this convention is given in the CLtL2 chapter on predicates.

Whichever convention your project wishes to use, be consistent and use only that convention in the entire project.

Do not use these boolean indicators in functions that do not return booleans and variables that are not boolean-valued.

Intent not Content

You should name a variable according to the high-level concept that it represents (intent), not according to the low-level implementation details of how the concept is represented (content).

Thus, you should avoid embedding data structure or aggregate type names, such as list, array, or hash-table as part of the variable names, unless you're writing a generic algorithm that applies to arbitrary lists, arrays, hash-tables, etc. In that case it's perfectly OK to name a variable list or array.

For example, if a variable's value is always a row (or is either a row or NIL), it's good to call it row or first-row or something like that.

Be consistent. If a variable is named row in one function, and its value is being passed to a second function, then call it row rather than, say, value.

Documentation

Topics related to in-code LFE documentation.

Docstrings

First and foremost, document everything.

You should use document strings (a.k.a. "docstrings") on all visible functions to explain how to use your code.

Unless some bit of code is painfully self-explanatory, document it with a documentation string.

Documentation strings are destined to be read by the programmers who use your code. They can be extracted from functions, types, classes, variables and macros, and displayed by programming tools, such as IDEs, or by REPL queries; web-based documentation or other reference works can be created based on them. Documentation strings are thus the perfect locus to document your API. They should describe how to use the code (including what pitfalls to avoid), as opposed to how the code works (and where more work is needed), which is what you'll put in comments.

Supply a documentation string when defining top-level functions, records, classes, variables and macros. Generally, add a documentation string wherever the language allows.

For functions, the docstring should describe the function's contract: what the function does, what the arguments mean, what values are returned, what conditions the function can signal. It should be expressed at the appropriate level of abstraction, explaining the intended meaning rather than, say, just the syntax.

Some LFE forms do not accept docstrings, in which case a preceding code comment should be used instead.

(defun small-prime-number? (n)
  "Return true if N, an integer, is a prime number. Otherwise, return false."
  ((n) (when (< n 4))
   (>= n 2))
  ((n) (when (== 0 (rem n 2)))
   'false)
  ((n)
   (lists:all #'not/1
              (lists:map (lambda (x) (== 0 (rem n x)))
                         (lists:seq 3 (trunc (math:sqrt n)))))))
(defmacro is (bool-expression)
  "Assert bool-expression evaluates to 'true."
  `(assert ,bool-expression))
;;; This record tracks test results and is ulimately used when reporting the
;;; status of completed tests.
(defrecord state
  (status (orddict:new))
  test-type
  (ok 0)
  (fail 0)
  (err 0)
  (skip 0)
  (cancel 0)
  (time 0))

A long docstring may usefully begin with a short, single-sentence summary, followed by the larger body of the docstring.

Text in successive lines of docstrings are indented two spaces, aligned with the open quote in the first line of documentation, not with the first character of the text.

Code Comments

Comments are explanations to the future maintainers of the code. Even if you're the only person who will ever see and touch the code, even if you're either immortal and never going to quit, or unconcerned with what happens after you leave (and have your code self-destruct in such an eventuality), you may find it useful to comment your code. Indeed, by the time you revisit your code, weeks, months or years later, you will find yourself a different person from the one who wrote it, and you will be grateful to the previous you for making the code readable.

Comment anything complicated so that the next developer can understand what's going on.

Also use comments as a way to guide those who read the code, so they know what to find where.

Code comments in LFE, as in most Lisp dialects, begin with a semi-colon, with their number having conventional semantic value:

  • Four Semi-colons: These are used for file headers and important comments that apply to large sections of code in a source file.
  • Three Semi-colons: These are used to begin comments that apply to just one top-level form or small group of top-level forms.
  • Two Semi-colons: These are used inside a top-level form, for comments appearing between lines. For code that uses unobvious forms to accomplish a task, you must include a comment stating the purpose of the form and the task it accomplishes.
  • One Semi-colon: This is used for parenthetical remark and only occurs at the end of a line. You should use spaces to separate the comment from the code it refers to so the comment stands out. You should try to vertically align consecutive related end-of-line comments.

For all comments, there should be a space between the semicolon and the text of the comment.

;;;; File-level comments or comments for large sections of code.
(defmodule math-n-things
  (export
   (utility-function 0)
   ...
   (small-prime-number? 1)
   (large-prime-number? 1)
   ...))

;;; The functions in this section are utility in nature, supporting others in
;;; the module. More details on their intended use cases are availale here:
;;; * https://some.url/

(defun utility-function ()
  ...)

;;; Prime numbers section

(defun small-prime-number? (n)
  "Return true if N, an integer, is a prime number. Otherwise, return false."
  ((n) (when (< n 4))  ; parenthetical remark here
   (>= n 2))           ; continuation of the remark
  ((n) (when (== 0 (rem n 2)))
   'false)             ; different remark here
  ((n)
   ;; Comment that applies to a section of code.
   (lists:all #'not/1
              (lists:map (lambda (x) (== 0 (rem n x)))
                         (lists:seq 3 (trunc (math:sqrt n)))))))

(defun large-prime-number? (n)
  ...)

Attention Required

For comments requiring special attention, such as incomplete code, todo items, questions, breakage, and danger, include a TODO or XXX comment indicating the type of problem, its nature, and any notes on how it may be addressed.

The comments begin with TODO or XXX in all capital letters, followed by the name, e-mail address, or other identifier of the person with the best context about the problem referenced by the TODO or XXX. The main purpose is to have a consistent TODO or XXX that can be searched to find out how to get more details upon request. A TODO or XXX is not a commitment that the person referenced will fix the problem. Thus when you create a TODO or XXX, it is almost always your name that is given.

Generally, TODO and XXX commands are differentiated in that TODO items represent normal code tasks around such things as incomplete features and XXX items represent a bug, potential bug, pitfalls, incorrectness, inelegance, uncertainty about part of the code, etc. Common synonyms for XXX include BUG, FIXME and sometimes HACK (this last especially for incorrectness or inelegance).

When signing comments, you should use your username (for code within the company) or full email address (for code visible outside the company), not just initials.

;; --- TODO (alice@gmail.com): Refactor to provide a better API.
;; --- TODO (bob): Remove this code after release 1.7 or before 2012-11-30.

If there is an associated issue or bug ticket with the given TODO or XXX item, be sure to include that in a following line:

;; --- XXX (carol): There is a serious issue here, causing problems in other
;;                  areas of the code. We haven't decided upon the best
;;                  approach yet. See the following ticket for details:
;;
;;                   * https://github.com/examplecom/api/issues/42
;;

Data Representation

Notes on basic data structures.

Lists

Use the appropriate functions when manipulating lists.

For simple access to list data, you can use car, cdr, cadr, etc. to access list elements and segments. For common pattern-matching in function heads, receive, let, etc., use cons to access the head and tail of a list (e.g., (,head . ,tail)).

Additionally, don't forget the lists Erlang module for accessing list elements.

You should avoid using a list as anything besides a container of elements of like type. Avoid using a list as method of passing multiple separate values of different types in and out of function calls. Sometimes it is convenient to use a list as a little ad hoc structure, i.e. "the first element of the list is a foo, and the second is a bar", but this should be used minimally since it gets harder to remember the little convention. You should only use a list that way when destructuring the list of arguments from a function, or creating a list of arguments to which to apply a function.

The proper way to pass around an object comprising several values of heterogeneous types is to use a record created via defrecord.

Tuples and Proplists

Do not align keys and values in property lists; instead, simply use the standard Lisp formatting (e.g, as provided by the LFE Emacs formatter).

Bad:

'(#(k1            v1)
  #(key2          value2)
  #(key-the-third value-the-third)
  #(another       one))

Better:

'(#(k1 v1)
  #(key2 value2)
  #(key-the-third value-the-third)
  #(another one))

Maps

Do not align keys and values in maps. Note, however, that the LFE Emacs formatter doesn't currently indent maps properly.

Bad:

'#m(k1            v1
    key2          value2
    key-the-third value-the-third
    another       one)

Also bad (formatted with the LFE Emacs formatter):

'#m(k1 v1
       key2          value2
       key-the-third value-the-third
       another       one)

Better:

#m(k1 v1
   key2 value2
   key-the-third value-the-third
   another one)

Records

Use records as the principle data structure

Use records as the principle data structure in messages. A record is a tagged tuple and was introduced in Erlang version 4.3 and thereafter.

If the record is to be used in several modules, its definition should be placed in a header file (with suffix .lfe) that is included from the modules. If the record is only used from within one module, the definition of the record should be in the beginning of the file the module is defined in.

The record features of LFE can be used to ensure cross module consistency of data structures and should therefore be used by interface functions when passing data structures between modules.

Use selectors and constructors

Use the record macros provided by LFE for managing instances of records. Don't use matching that explicitly assumes that the record is a tuple.

Bad:

(defun demo ()
  (let* ((joe (make-person name "Joe" age 29))
         (`#(person ,name ,age) joe))
    ...
    ))

Good:

(defun demo ()
  (let* ((joe (make-person name "Joe" age 29))
         (name-2 (person-name joe)))
    ...
    ))

Errors

Separate error handling and normal case code

Don't clutter code for the "normal case" with code designed to handle exceptions. As far as possible you should only program the normal case. If the code for the normal case fails, your process should report the error and crash as soon as possible. Don't try to fix up the error and continue. The error should be handled in a different process.

Clean separation of error recovery code and normal case code should greatly simplify the overall system design.

The error logs which are generated when a software or hardware error is detected will be used at a later stage to diagnose and correct the error. A permanent record of any information that will be helpful in this process should be kept.

Identify the error kernel

One of the basic elements of system design is identifying which part of the system has to be correct and which part of the system does not have to be correct.

In conventional operating system design the kernel of the system is assumed to be, and must be, correct, whereas all user application programs do not necessarily have to be correct. If a user application program fails this will only concern the application where the failure occurred but should not affect the integrity of the system as a whole.

The first part of the system design must be to identify that part of the system which must be correct; we call this the error kernel. Often the error kernel has some kind of real-time memory resident data base which stores the state of the hardware.

Processes, Servers and Messages

Processes

Implement a process in one module

Code for implementing a single process should be contained in one module. A process can call functions in any library routines but the code for the "top loop" of the process should be contained in a single module. The code for the top loop of a process should not be split into several modules - this would make the flow of control extremely difficult to understand. This does not mean that one should not make use of generic server libraries, these are for helping structuring the control flow.

Conversely, code for no more than one kind of process should be implemented in a single module. Modules containing code for several different processes can be extremely difficult to understand. The code for each individual process should be broken out into a separate module.

Use processes for structuring the system

Processes are the basic system structuring elements. But don't use processes and message passing when a function call can be used instead.

Registered processes

Registered processes should be registered with the same name as the module. This makes it easy to find the code for a process.

Only register processes that should live a long time.

Assign exactly one parallel process to each true concurrent activity in the system

When deciding whether to implement things using sequential or parallel processes then the structure implied by the intrinsic structure of the problem should be used. The main rule is:

"Use one parallel process to model each truly concurrent activity in the real world"

If there is a one-to-one mapping between the number of parallel processes and the number of truly parallel activities in the real world, the program will be easy to understand.

Each process should only have one "role"

Processes can have different roles in the system, for example in the client-server model.

As far as possible a process should only have one role, i.e. it can be a client or a server but should not combine these roles.

Other roles which process might have are:

Supervisor: watches other processes and restarts them if they fail. Worker: a normal work process (can have errors). Trusted Worker: not allowed to have errors.

Use the process dictionary with extreme care

Do not use get and put etc. unless you know exactly what you are doing! Use get and put etc., as little as possible.

A function that uses the process dictionary can be rewritten by introducing a new argument.

Don't program like this:

(defun tokenize
  ((`(,head . ,tail))
    ...)
  (('())
    (case (get-characters-from-device (get 'device))
      ('eof
        '())
      (`#(value ,chars)
        (tokenize chars)))))

The correct solution:

(defun tokenize
  ((device (,head . ,tail))
    ...)
  ((device '())
    (case (get-characters-from-device device)
      ('eof
        '())
      (`#(value, chars)
        (tokenize device chars)))))

The use of get and put will cause a function to behave differently when called with the same input at different occasions. This makes the code hard to read since it is non-deterministic. Debugging will be more complicated since a function using get and put is a function of not only of its argument, but also of the process dictionary. Many of the run time errors in LFE (for example bad_match) include the arguments to a function, but never the process dictionary.

Servers

Use generic functions for servers and protocol handlers wherever possible

In many circumstances it is a good idea to use generic server programs such as the generic server implemented in the standard libraries. Consistent use of a small set of generic servers will greatly simplify the total system structure.

The same is possible for most of the protocol handling software in the system.

Write tail-recursive servers

All servers must be tail-recursive, otherwise the server will consume memory until the system runs out of it.

Don't program like this:

(defun loop ()
  (receive
    (`#(msg1 ,msg1)
     ...
     (loop))
    ('stop 'true)
    (other
     (logger:error "Process ~w got unknown msg ~w~n"
                   `(,(self) ,other))
     (loop)))
  ;; don't do this! This is not tail-recursive!
  (io:format "Server going down" '()))

This is a correct solution:

(defun loop ()
  (receive
    (`#(msg1 ,msg1)
     ...
     (loop))
    ('stop
     (io:format "Server going down" '()))
    (other
     (logger:error "Process ~w got unknown msg ~w~n"
                   `(,(self) ,other))
     (loop))))

If you use some kind of server library, for example generic, you automatically avoid doing this mistake.

Messages

Tag messages

All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.

Don't program like this:

(defun loop (state)
  (receive
    ...
    (`#(,mod ,funcs ,args)
     (erlang:apply mod funcs args)
     (loop state))
    ...))

The new message `#(get_status_info ,from ,option) will introduce a conflict if it is placed below the `#(,mod ,funcs ,args) message.

If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.

This is a good solution:

(defun loop (state)
  (receive
    ...
    (`#(execute ,mod ,funcs ,args)
     (erlang:apply mod funcs args)
     (loop state))
    (`#(get_status_info ,from ,option)
     (! from `#(status_info ,(get-status-info option state)))
     (loop state))
    ...))

Use tagged return values

Use tagged return values.

Don't program like this:

(defun keysearch
  ((key `(#(,key ,value) . ,tail))
    value)
  ((key `(cons `#(,wrong-key ,wrong-value) . ,tail))
    (keysearch key '()))
  ((key '())
    'false))

Then (tuple key, value) cannot contain the false value.

This is the correct solution:

(defun keysearch
  ((key `(#(,key ,value) . ,tail))
    `#(value ,value))
  ((key `(#(,wrong-key ,wrong-value) . ,tail))
    (keysearch key '()))
  ((key '())
    'false))

Flush unknown messages

Every server should have an Other alternative in at least one receive statement. This is to avoid filling up message queues. Example:

(defun main-loop ()
  (receive
    (`#(msg1 ,msg1)
     ...
     (main-loop))
    (`#(msg2 ,msg2)
     ...
     (main-loop))
    (other  ; flush the message queue
     (logger:error "Process ~w got unknown msg ~w~n" `(,(self) ,other))
     (main-loop))))

Interface functions

Use functions for interfaces whenever possible, avoid sending messages directly. Encapsulate message passing into interface functions. There are cases where you can't do this.

The message protocol is internal information and should be hidden to other modules.

Example of interface function:

(defmodulee fileserver
  (export
   (start 0)
   (stop 0)
   (open-file 1)
   ...))

(defun open-file (server-pid filename)
  (! serever-pid `#(open-file-request ,filename))
  (receive
    (`#(open-file-response ,result) result)))

...

Time-outs

Be careful when using after in receive statements. Make sure that you handle the case when the message arrives later.

Trapping exits

As few processes as possible should trap exit signals. Processes should either trap exits or they should not. It is usually very bad practice for a process to "toggle" trapping exits.

Software Components

From the smallest chunks of code to a completed project.

Flow Control

if Branches

Large conditional expressions and deeply nested blocks of code are harder to read, so should be factored out into functions.

For example, this:

(if (and (fuelled? rocket)
         (lists:all #'strapped-in?
                    (crew rocket))
         (sensors-working? rocket))
  (launch rocket)
  (! pid `#(err "Aborting launch.")))

Should be refactored to something like this:

(defun rocket-ready? (rocket)
  (and (fuelled? rocket)
       (lists:all #'strapped-in?
                  (crew rocket))
       (sensors-working? rocket)))

(if (rocket-ready-p rocket)
  (launch rocket)
  (! pid `#(err "Aborting launch.")))

case Branches

Don't write complex case statements with deeply nested branching. Instead, split these into functions, too, pattern-matching in the function heads.

Functions

Keep Functions Small

Keep functions small, focused on one thing. If you have six separate tasks being performed by a function, create six functions for these.

Group Functions Logically

Try to always separate unexported and exported functions in groups, with the exported ones first, unless it helps readability and code discovery.

Modules

When defining modules in LFE, put exported functions and their arities on separate lines. Optionally, functions of the same name with different arity may be put on the same line. Functions within a single export call should be sorted alphabetically.

Do not use (export all); explicitly exporting functions constitutes the conventional contract for clients utilising the module.

Very bad:

(defmodule maths
  (export all))

Bad:

(defmodule maths
  (export (factorial 2)
          (large-prime-number? 1)
          (small-prime-number? 1)
          (ackermann 2)
          (factorial 1)))

Better:

(defmodule maths
  (export
   (ackermann 2)
   (factorial 1) (factorial 2)
   (large-prime-number? 1)
   (small-prime-number? 1)))

If you have a public API with groups of related functions in a module, you may indicate their relatedness with separate exports:

(defmodule maths
  (export
   (util-func 1)
   (other-util 2))
  (export
   (ackermann 2)
   (factorial 1) (factorial 2)
   (large-prime-number? 1)
   (small-prime-number? 1)))

With Pseudo-Packages

If you are using the LFE rebar3 plugin, then you also have the flexibility of organising your project's source code into sub-directories under your source directory (see the projects section for more information).

In that case, you would define your module like so:

(defmodule project.subdir.maths
  (export
   (ackermann 2)
   (factorial 1) (factorial 2)
   (small-prime-number? 1)
   (large-prime-number? 1)))

Since there is no support in Erlang and LFE for actual packages, the dotted name is actually a module. As such, when referencing this module elsewhere, use import aliases to improve readability):

(defmodule client
  (export
   (some-func 0))
  (import
   (from project.subdir.maths
         (small-prime-number? 1))))

(defun some-func ()
  (small-prime-number? 100))

Or, if you need to avoid a name collision between the imported function and one in the client module:

(defmodule client
  (export
   (some-func 0))
  (import
   (rename project.subdir.maths
           ((small-prime-number? 1) small-prime?))))

(defun some-func ()
  (small-prime? 100))

When to Create

If some portion of your code is reusable enough to be a module then the maintenance gains are really worth the overhead of splitting it out with separate tests and docs.1

Gains for separating code into separate modules include, but are not limited to:

  • Easier reuse in other parts of the software being developed.
  • Increased ability to reason about problems due to increased simplicity and separation of concerns.
  • Great clarity and understanding of the system as a whole.

A good general workflow around module creation:

  1. Start small and remain focused on the problem at hand.
  2. Write just the functions you need.
  3. Keep the functions small and limit them to one specific chunk of functionality (do one thing and do it well).
  4. Make incremental changes as needed.

For new code:

  1. Experiment with in the LFE REPL by defining your function and then calling with different values (expected and otherwise).
  2. When it works in the REPL, create a test module in ./test and paste the function calls in a test case.
  3. Create a new module in ./src and paste the final form of your function from the REPL.
  4. Ensure the tests pass successfully for the new module.

Build your libraries using this approach


  1. Parts of this page were adapted from a Github Gist by James Halliday.

Libraries

Use Before You Write

Look for libraries that solve the problems you are trying to solve before embarking on a project. 1 Making a project with no dependencies is not some sort of virtue. It doesn’t aid portability and it doesn’t help when it comes to turning a Lisp program into an executable.

Writing a library that solves the same problem as another hurts consolidation. Generally, you should only do this if you are going to make it worth it: Good, complete documentation, examples, and a well-designed website are – taken together – a good reason to write an alternative library to an existing one.

As always, check the licensing information of the libraries you use for incompatibilities.

Writing Libraries

Before starting a project, think about its structure: Does every component have to be implemented within the project? Or are there parts that might be usable by others? If so, split the project into libraries.

If you set out to write a vast monolith and then lose interest, you will end up with 30% of an ambitious project, completely unusable to others because it’s bound to the rest of the unfinished code.

If you think of your project as a collection of independent libraries, bound together by a thin layer of domain-specific functionality, then if you lose interest in a project you will have left a short trail of useful libraries for others to use and build upon.

In short: write many small libraries.


  1. This entire page was adapted from the lisp-lang.org Style Guide's General Guidelines.

Projects

Use the established best practices for Erlang project creation, adapted for LFE.

These have been used when defining the LFE new project templates in the rebar3 plugin. That is probably the best way to get consistent results when creating the most common types of LFE projects (e.g., main-scripts, escripts, libraries, OTP applications, and OTP releases).

With Pseudo-Packages

While Erlang and LFE do not support packages, it is possible to use the rebar3 LFE plugin to simulate packages, complete with project directory structures that consolidate specific functionality in collections of sub-directories. These will be detected at compile-time when you use rebar3 lfe compile and from these, proper Erlang-compatible modules will be compiled (with dotted names preserving the hierarchy). Consider your project's organisation carefully when creating your sub-directory structure.

Here is a good example for a game project's directory structure:

 ├── LICENSE
 ├── README.md
 ├── rebar.config
 ├── src
 │  ├── title.app.src
 │  └── title
 │     ├── config.lfe
 │     ├── db.lfe
 │     ├── graphics
 │     │  ├── mesh.lfe
 │     │  ├── obj.lfe
 │     │  └── gl.lfe
 │     └── logging.lfe
 ├── test
 ...

The modules under the src/title directory can be represented in a more abstract hierarchy:

title
  title.graphics
    title.graphics.mesh
    title.graphics.obj
    title.graphics.gl
  title.config
  title.logging
  title.db

Keep in mind that the LFE plugin will flatten this structure into sibling files compiled to the ebin directory:

title.app
title.config.beam
title.db.beam
title.graphics.mesh.beam
title.graphics.obj.beam
title.graphics.gl.beam
title.logging.beam

Software Engineering

General best practices around software engineering.

Principles

There are some basic principles for team software development that every developer must keep in mind. Whenever the detailed guidelines are inadequate, confusing or contradictory, refer back to these principles for guidance:

  • Every developer's code must be easy for another developer to read, understand, and modify — even if the first developer isn't around to explain it. (This is the "hit by a truck" principle.)
  • Everybody's code should look the same. Ideally, there should be no way to look at lines of code and recognize it as "Alice's code" by its style.
  • Be precise.
  • Be concise.
  • Keep it simple.
  • Use the smallest hammer for the job.
  • Use common sense.
  • Keep related code together. Minimize the amount of jumping around someone has to do to understand an area of code.

Priorities

When making decisions about how to write a given piece of code, aim for the following in this priority order:

  • Usability by the customer
  • Debuggability/Testability
  • Readability/Comprehensibility
  • Extensibility/Modifiability
  • Efficiency (of the LFE code at runtime)

Most of these are obvious.

Usability by the customer means that the system has to do what the customer requires; it has to handle the customer's transaction volumes, uptime requirements; etc.

For the LFE efficiency point, given two options of equivalent complexity, pick the one that performs better. (This is often the same as the one that conses less, i.e. allocates less storage from the heap.)

Given two options where one is more complex than the other, pick the simpler option and revisit the decision only if profiling shows it to be a performance bottleneck.

However, avoid premature optimisation. Don't add complexity to speed up something that runs rarely, since in the long run, it matters less whether such code is fast.

Architecture

To build code that is robust and maintainable, it matters a lot how the code is divided into components, how these components communicate, how changes propagate as they evolve, and more importantly how the programmers who develop these components communicate as these components evolve.

If your work affects other groups, might be reusable across groups, adds new components, has an impact on other groups (including QA or Ops), or otherwise isn't purely local, you must write it up using at least a couple of paragraphs, and get a design approval from the other parties involved before starting to write code — or be ready to scratch what you have when they object.

If you don't know or don't care about these issues, ask someone who does.

Feedback and Docs Bugs

If you would like to provide feedback about this book, we would welcome the chance to improve the experience for everyone. Please create a ticket in the Github issue tracker. Be sure to give a full description so that we can best help you!