Learn LFE

Lisp Flavoured Erlang: a homoiconic Lisp-2 with real macros that compiles to Core Erlang and runs on the BEAM.

Hello, S-expressions

Read the parentheses, start the REPL, and write your first module.

LFE (Lisp Flavoured Erlang) is a Lisp dialect created by Robert Virding, one of the original co-inventors of Erlang. It compiles to Core Erlang and runs on the BEAM, so it shares Erlang's processes, OTP, and standard library while giving you S-expression syntax and real Lisp macros.

In LFE everything is an expression written as a parenthesised list. The first element is the operator and the rest are arguments, so (+ 1 2) means "call + with 1 and 2". This is prefix notation: there is no operator precedence to remember because the parentheses make grouping explicit.

(+ 1 2 3)          ; => 6, + takes any number of args
(* (+ 1 2) 4)      ; => 12, inner form is evaluated first
(io:format "~p~n" (list (+ 40 2)))  ; prints 42

The fastest way to explore is the REPL. Unlike the plain Erlang shell, the LFE shell (lfe or rebar3 lfe repl) can define functions and even macros interactively:

lfe> (* 6 7)
42
lfe> (set greeting "hello")
"hello"
lfe> (++ greeting ", world")
"hello, world"

A real program lives in a module. The conventional first program looks like this - note that defmodule, export, and defun are themselves just forms:

(defmodule hello
  (export (run 0)))

(defun run ()
  (io:format "Hello, BEAM!~n"))

The (export (run 0)) says the function run with arity 0 (zero arguments) is callable from outside the module. Arity is part of a function's identity on the BEAM, which is why exports always list name/arity pairs. Compile and call it from the shell with (c "hello.lfe") then (hello:run).

Data types and binding

Atoms, tuples, lists, maps, binaries, and let.

LFE uses the same data types as Erlang, just written as S-expressions. Getting comfortable with them is most of the battle.

Atoms are constant names that stand for themselves, like ok, error, or true. Integers and floats behave as you'd expect, and integers are arbitrary precision. Strings are written with double quotes and are, by Erlang tradition, lists of character codes.

Tuples are fixed-size groups written with #(...) and are the usual way to bundle a few related values:

#(ok 42)            ; a 2-tuple, often a result tag
#(point 3 4)        ; a tagged record-ish tuple

Lists are written with parentheses as data - but because parentheses also mean "call", you must quote a literal list so it isn't evaluated. (list ...) builds one explicitly:

'(1 2 3)            ; quoted literal list
(list 1 2 3)        ; built at runtime, => (1 2 3)
(cons 0 '(1 2 3))   ; => (0 1 2 3)
(car '(1 2 3))      ; => 1   (head)
(cdr '(1 2 3))      ; => (2 3) (tail)

Maps are key/value dictionaries written with (map k1 v1 k2 v2 ...):

(let ((m (map 'name "Ada" 'lang 'lfe)))
  (mref m 'name))   ; => "Ada"

Binaries hold raw bytes and are written with #"..." or the binary form; they are how LFE handles efficient byte and text data.

Variables are introduced with let. Each binding is a (name value) pair, and the bindings are visible inside the body. Like all BEAM languages, variables are single-assignment: once bound in a scope, a name cannot be reassigned.

(let ((x 10)
      (y 20))
  (+ x y))          ; => 30

For sequential bindings where a later one depends on an earlier one, use let*:

(let* ((x 10)
       (y (* x 2)))
  y)                ; => 20

Functions, clauses, and pattern matching

defun with multiple clauses, guards, and destructuring.

Functions are defined with defun. A simple function lists its parameters and a body:

(defun double (x)
  (* x 2))

The real power comes from pattern matching. A function can have several clauses, each with a pattern of arguments; LFE tries them top to bottom and runs the first that matches. This replaces most if/switch logic you'd write in other languages:

(defun greet
  ((#(en name)) (++ "Hello, " name))
  ((#(fr name)) (++ "Bonjour, " name))
  ((#(_  name)) (++ "Hi, " name)))

Here each clause is wrapped in its own parentheses: (pattern body). The patterns destructure a tuple, binding name from inside it. The underscore _ is a wildcard that matches anything without binding it.

Classic recursion over lists falls out naturally from matching the empty list against a head/tail pattern:

(defun sum
  (('()) 0)
  (((cons h t)) (+ h (sum t))))

(sum '(1 2 3 4))    ; => 10

Guards add boolean conditions to a clause with (when ...). Guards may only use a restricted set of side-effect-free tests (comparisons, type checks like is_integer, arithmetic):

(defun classify
  ((n) (when (< n 0)) 'negative)
  ((0)                'zero)
  ((n) (when (> n 0)) 'positive))

You can also match outside a function with case, which tests one value against a series of patterns:

(defun describe (result)
  (case result
    (#(ok value)    (io:format "got ~p~n" (list value)))
    (#(error reason)(io:format "failed: ~p~n" (list reason)))
    (_              (io:format "unknown~n" '()))))

This #(ok value) / #(error reason) convention is the idiomatic way BEAM code reports success and failure, and LFE matches on it cleanly.

Processes, messages, and OTP

Spawn lightweight processes and talk to them with send and receive.

Because LFE compiles to Core Erlang, it inherits Erlang's concurrency model with zero penalty. A process is a lightweight, isolated unit of execution - the BEAM happily runs millions of them - that shares nothing and communicates only by sending messages.

You create one with spawn, send to it with ! (or send), and a process pulls messages from its mailbox with receive, which pattern-matches just like a function:

(defun loop ()
  (receive
    (#(add x y)
     (io:format "~p + ~p = ~p~n" (list x y (+ x y)))
     (loop))
    ('stop
     (io:format "bye~n" '()))))

(defun start ()
  (spawn 'my-module 'loop '()))

Use it from the shell. (! pid msg) (or (send pid msg)) drops a message into the process's mailbox; the running process matches it and loops to wait for the next one:

lfe> (set pid (start))
lfe> (! pid #(add 2 3))
2 + 3 = 5
lfe> (! pid 'stop)
bye

Notice the recursion: after handling a message the process calls loop again, which is how a process keeps state and stays alive. Because each call is in tail position, this runs in constant stack space - a long-lived server is just an endlessly tail-recursive function.

For anything real you rarely hand-roll receive loops; you lean on OTP, Erlang's battle-tested framework of behaviours like gen_server (a generic stateful server) and supervisor (which restarts crashed children). LFE can implement OTP behaviours directly - the same supervision trees and "let it crash" fault tolerance that Erlang and Elixir use are available unchanged, just spelled in parentheses. This shared runtime is why LFE, Erlang, and Elixir modules can call each other freely inside one release.

Macros: code is data

Use defmacro and quasiquotation to extend the language itself.

LFE is homoiconic: your program is written using the very same lists, atoms, and tuples that the language manipulates as data. That property is what makes Lisp macros possible - a macro is a function that runs at compile time, receiving unevaluated code as data and returning new code to be compiled in its place. This is metaprogramming that plain Erlang never offered.

LFE is also a Lisp-2 (in the Common Lisp tradition): functions and variables live in separate namespaces, so a local variable named list doesn't shadow the list function.

Define a macro with defmacro. Quasiquotation - backquote ` to build a template, comma , to splice in a value - makes it easy to write the code a macro produces. Here is an unless that expands into an if:

(defmacro unless (test . body)
  `(if ,test
       'ok
       (progn ,@body)))

The . body captures all remaining forms as a list, and ,@ splices that list into place. Now unless looks and feels like a built-in control form:

(unless (== status 'ok)
  (io:format "something is wrong~n" '())
  (alert!))

You can inspect what a macro expands to, which is invaluable for debugging:

lfe> (macroexpand-1 '(unless x (f) (g)) $ENV)
(if x (quote ok) (progn (f) (g)))

Macros let you build little domain-specific languages, eliminate boilerplate, and capture patterns that a plain function can't - for example, anything that must control whether or how many times its arguments are evaluated. LFE macros are unhygienic (Common Lisp style), so a careful author uses generated unique names (via helpers in the standard macro toolkit) to avoid accidentally capturing a caller's variables.

Between pattern matching, BEAM concurrency, and real macros, LFE gives you Erlang's rock-solid runtime with a syntax that treats programs as the malleable data they truly are.