LFE (programming language)
View on Wikipedia| LFE | |
|---|---|
| Paradigm | Multi-paradigm: concurrent, functional |
| Family | Erlang, Lisp |
| Designed by | Robert Virding |
| Developer | Robert Virding |
| First appeared | 2008 |
| Stable release | 2.1.1
/ 6 January 2023 |
| Typing discipline | dynamic, strong |
| Implementation language | Erlang |
| OS | Cross-platform |
| License | Apache 2.0 |
| Filename extensions | .lfe .hrl |
| Website | lfe |
| Influenced by | |
| Erlang, Common Lisp, Maclisp, Scheme, Elixir, Clojure, Hy | |
| Influenced | |
| Joxa, Concurrent Schemer | |
Lisp Flavored Erlang (LFE) is a functional, concurrent, garbage collected, general-purpose programming language and Lisp dialect built on Core Erlang and the Erlang virtual machine (BEAM). LFE builds on Erlang to provide a Lisp syntax for writing distributed, fault-tolerant, soft real-time, non-stop applications. LFE also extends Erlang to support metaprogramming with Lisp macros and an improved developer experience with a feature-rich read–eval–print loop (REPL).[1] LFE is actively supported on all recent releases of Erlang; the oldest version of Erlang supported is R14.
History
[edit]
Initial release
[edit]Initial work on LFE began in 2007, when Robert Virding started creating a prototype of Lisp running on Erlang.[2] This work was focused primarily on parsing and exploring what an implementation might look like. No version control system was being used at the time, so tracking exact initial dates is somewhat problematic.[2]
Virding announced the first release of LFE on the Erlang Questions mail list in March 2008.[3] This release of LFE was very limited: it did not handle recursive letrecs, binarys, receive, or try; it also did not support a Lisp shell.[4]
Initial development of LFE was done with version R12B-0 of Erlang[5] on a Dell XPS laptop.[4]
| 1958 | 1960 | 1965 | 1970 | 1975 | 1980 | 1985 | 1990 | 1995 | 2000 | 2005 | 2010 | 2015 | 2020 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LISP 1, 1.5, LISP 2(abandoned) | |||||||||||||||
| Maclisp | |||||||||||||||
| Interlisp | |||||||||||||||
| MDL | |||||||||||||||
| Lisp Machine Lisp | |||||||||||||||
| Scheme | R5RS | R6RS | R7RS small | ||||||||||||
| NIL | |||||||||||||||
| ZIL (Zork Implementation Language) | |||||||||||||||
| Franz Lisp | |||||||||||||||
| muLisp | |||||||||||||||
| Common Lisp | ANSI standard | ||||||||||||||
| Le Lisp | |||||||||||||||
| MIT Scheme | |||||||||||||||
| XLISP | |||||||||||||||
| T | |||||||||||||||
| Chez Scheme | |||||||||||||||
| Emacs Lisp | |||||||||||||||
| AutoLISP | |||||||||||||||
| PicoLisp | |||||||||||||||
| Gambit | |||||||||||||||
| EuLisp | |||||||||||||||
| ISLISP | |||||||||||||||
| OpenLisp | |||||||||||||||
| PLT Scheme | Racket | ||||||||||||||
| newLISP | |||||||||||||||
| GNU Guile | |||||||||||||||
| Visual LISP | |||||||||||||||
| Clojure | |||||||||||||||
| Arc | |||||||||||||||
| LFE | |||||||||||||||
| Hy | |||||||||||||||
Motives
[edit]Robert Virding has stated that there were several reasons why he started the LFE programming language:[2]
- He had prior experience programming in Lisp.
- Given his prior experience, he was interested in implementing his own Lisp.
- In particular, he wanted to implement a Lisp in Erlang: not only was he curious to see how it would run on and integrate with Erlang, he wanted to see what it would look like.
- Since helping to create the Erlang programming language, he had had the goal of making a Lisp which was specifically designed to run on the BEAM and able to fully interact with Erlang/OTP.
- He wanted to experiment with compiling another language on Erlang. As such, he saw LFE as a means to explore this by generating Core Erlang and plugging it into the backend of the Erlang compiler.
Features
[edit]- A language targeting Erlang virtual machine (BEAM)
- Seamless Erlang integration: zero-penalty Erlang function calls (and vice versa)
- Metaprogramming via Lisp macros and the homoiconicity of a Lisp
- Common Lisp-style documentation via both source code comments and docstrings
- Shared-nothing architecture concurrent programming via message passing (Actor model)
- Emphasis on recursion and higher-order functions instead of side-effect-based looping
- A full read–eval–print loop (REPL) for interactive development and testing (unlike Erlang's shell, the LFE REPL supports function and macro definitions)
- Pattern matching
- Hot loading of code
- A Lisp-2 separation of namespaces for variables and functions
- Java inter-operation via JInterface and Erjang
- Scripting abilities with both
lfeandlfescript
Syntax and semantics
[edit]Symbolic expressions (S-expressions)
[edit]Like Lisp, LFE is an expression-oriented language. Unlike non-homoiconic programming languages, Lisps make no or little syntactic distinction between expressions and statements: all code and data are written as expressions. LFE brought homoiconicity to the Erlang VM.
Lists
[edit]In LFE, the list data type is written with its elements separated by whitespace, and surrounded by parentheses. For example, (list 1 2 'foo) is a list whose elements are the integers 1 and 2, and the atom [[foo|foo]]. These values are implicitly typed: they are respectively two integers and a Lisp-specific data type called a symbolic atom, and need not be declared as such.
As seen in the example above, LFE expressions are written as lists, using prefix notation. The first element in the list is the name of a form, i.e., a function, operator, or macro. The remainder of the list are the arguments.
Operators
[edit]The LFE-Erlang operators are used in the same way. The expression
(* (+ 1 2 3 4 5 6) 2)
evaluates to 42. Unlike functions in Erlang and LFE, arithmetic operators in Lisp are variadic (or n-ary), able to take any number of arguments.
Lambda expressions and function definition
[edit]LFE has lambda, just like Common Lisp. It also, however, has lambda-match to account for Erlang's pattern-matching abilities in anonymous function calls.
Erlang idioms in LFE
[edit]This section does not represent a complete comparison between Erlang and LFE, but should give a taste.
Pattern matching
[edit]Erlang:
1> {Len,Status,Msg} = {8,ok,"Trillian"}.
{8,ok,"Trillian"}
2> Msg.
"Trillian"
LFE:
lfe> (set (tuple len status msg) #(8 ok "Trillian"))
lfe> ;; or with LFE literal tuple syntax:
lfe> (set `#(,len ,status ,msg) #(8 ok "Trillian"))
#(8 ok "Trillian")
lfe> msg
"Trillian"
List comprehensions
[edit]Erlang:
1> [trunc(math:pow(3,X)) || X <- [0,1,2,3]].
[1,3,9,27]
LFE:
lfe> (list-comp
((<- x '(0 1 2 3)))
(trunc (math:pow 3 x)))
(1 3 9 27)
Or idiomatic functional style:
lfe> (lists:map
(lambda (x) (trunc (math:pow 3 x)))
'(0 1 2 3))
(1 3 9 27)
Guards
[edit]Erlang:
right_number(X) when X == 42; X == 276709 ->
true;
right_number(_) ->
false.
LFE:
(defun right-number?
((x) (when (orelse (== x 42) (== x 276709)))
'true)
((_) 'false))
cons'ing in function heads
[edit]Erlang:
sum(L) -> sum(L,0).
sum([], Total) -> Total;
sum([H|T], Total) -> sum(T, H+Total).
LFE:
(defun sum (l) (sum l 0))
(defun sum
(('() total) total)
(((cons h t) total) (sum t (+ h total))))
or using a ``cons`` literal instead of the constructor form:
(defun sum (l) (sum l 0))
(defun sum
(('() total) total)
((`(,h . ,t) total) (sum t (+ h total))))
Matching records in function heads
[edit]Erlang:
handle_info(ping, #state {remote_pid = undefined} = State) ->
gen_server:cast(self(), ping),
{noreply, State};
handle_info(ping, State) ->
{noreply, State};
LFE:
(defun handle_info
(('ping (= (match-state remote-pid 'undefined) state))
(gen_server:cast (self) 'ping)
`#(noreply ,state))
(('ping state)
`#(noreply ,state)))
Receiving messages
[edit]Erlang:
universal_server() ->
receive
{become, Func} ->
Func()
end.
LFE:
(defun universal-server ()
(receive
((tuple 'become func)
(funcall func))))
or:
(defun universal-server ()
(receive
(`#(become ,func)
(funcall func))))
Examples
[edit]Erlang interoperability
[edit]Calls to Erlang functions take the form (<module>:<function> <arg1> ... <argn>):
(io:format "Hello, World!")
Functional paradigm
[edit]Using recursion to define the Ackermann function:
(defun ackermann
((0 n) (+ n 1))
((m 0) (ackermann (- m 1) 1))
((m n) (ackermann (- m 1) (ackermann m (- n 1)))))
Composing functions:
(defun compose (f g)
(lambda (x)
(funcall f
(funcall g x))))
(defun check ()
(let* ((sin-asin (compose #'sin/1 #'asin/1))
(expected (sin (asin 0.5)))
(compose-result (funcall sin-asin 0.5)))
(io:format "Expected answer: ~p~n" (list expected))
(io:format "Answer with compose: ~p~n" (list compose-result))))
Concurrency
[edit]Message-passing with Erlang's light-weight "processes":
(defmodule messenger-back
(export (print-result 0) (send-message 2)))
(defun print-result ()
(receive
((tuple pid msg)
(io:format "Received message: '~s'~n" (list msg))
(io:format "Sending message to process ~p ...~n" (list pid))
(! pid (tuple msg))
(print-result))))
(defun send-message (calling-pid msg)
(let ((spawned-pid (spawn 'messenger-back 'print-result ())))
(! spawned-pid (tuple calling-pid msg))))
Multiple simultaneous HTTP requests:
(defun parse-args (flag)
"Given one or more command-line arguments, extract the passed values.
For example, if the following was passed via the command line:
$ erl -my-flag my-value-1 -my-flag my-value-2
One could then extract it in an LFE program by calling this function:
(let ((args (parse-args 'my-flag)))
...
)
In this example, the value assigned to the arg variable would be a list
containing the values my-value-1 and my-value-2."
(let ((`#(ok ,data) (init:get_argument flag)))
(lists:merge data)))
(defun get-pages ()
"With no argument, assume 'url parameter was passed via command line."
(let ((urls (parse-args 'url)))
(get-pages urls)))
(defun get-pages (urls)
"Start inets and make (potentially many) HTTP requests."
(inets:start)
(plists:map
(lambda (x)
(get-page x)) urls))
(defun get-page (url)
"Make a single HTTP request."
(let* ((method 'get)
(headers '())
(request-data `#(,url ,headers))
(http-options ())
(request-options '(#(sync false))))
(httpc:request method request-data http-options request-options)
(receive
(`#(http #(,request-id #(error ,reason)))
(io:format "Error: ~p~n" `(,reason)))
(`#(http #(,request-id ,result))
(io:format "Result: ~p~n" `(,result))))))
References
[edit]- ^ Virding, Robert. "Lisp Flavored Erlang" (PDF). Erlang Factory. Retrieved 2014-01-17.
- ^ a b c "LFE History on the Lisp Flavored Erlang mail list". Retrieved 2014-05-28.
- ^ "LFE announcement on Erlang Questions mail list". Retrieved 2014-01-17.
- ^ a b Armstrong, Joe; Virding, Robert (2013-12-30). "Hardware used in the development of Erlang and LFE" (Email exchange). Interviewed by Duncan McGreggor. Retrieved 2014-01-17.
- ^ "Follow-up to LFE announcement on Erlang Questions mail list". Retrieved 2014-01-17.
External links
[edit]LFE (programming language)
View on GrokipediaHistory
Origins and motives
Lisp Flavored Erlang (LFE) originated from the efforts of Robert Virding, a co-creator of Erlang and a longtime Lisp enthusiast who first encountered Lisp around 1980-1981 while pursuing a PhD in theoretical physics at Stockholm University, where he used it for algebraic computations. As an early member of Ericsson's Computer Science Laboratory, Virding contributed significantly to Erlang's design, libraries, and compiler, while also experimenting with Lisp implementations, including a version of Lisp Machine Flavors on Portable Standard Lisp. In 2007, after two decades of Erlang development, Virding began prototyping a Lisp dialect on the Erlang virtual machine (BEAM) as a personal project during his spare time, driven by his passion for language implementation and the challenge of blending Lisp traditions with Erlang's ecosystem.[3][5][6] The primary motives for creating LFE stemmed from Virding's desire to merge Lisp's core strengths—such as homoiconicity, powerful macros, and its rich functional programming heritage—with Erlang's robust concurrency model, fault-tolerance mechanisms, and distributed computing capabilities, enabling the construction of highly reliable systems without sacrificing Lisp's expressive power. Virding sought to explore Lisp-2 semantics, where functions and variables occupy separate namespaces akin to Common Lisp, directly on the BEAM VM, avoiding the need to rewrite Erlang's runtime while ensuring full interoperability with Erlang and the Open Telecom Platform (OTP) framework. This combination aimed to provide Lisp programmers access to Erlang's actor-based concurrency for scalable, fault-tolerant applications, while allowing Erlang developers to leverage Lisp's metaprogramming features.[7][8][9] Early experimentation centered on implementing Lisp parsing and evaluation atop Erlang, with a key focus on translating Lisp S-expressions into the Core Erlang abstract syntax tree (AST) to leverage the existing BEAM compiler backend. This approach allowed Virding to prototype a functional Lisp dialect that compiled seamlessly to Core Erlang, maintaining 100% compatibility and enabling smooth integration with Erlang's tools and libraries from the outset.[3][6]Development timeline
Development of Lisp Flavored Erlang (LFE) began with prototype experimentation in 2007, focusing on Lisp parsing and implementation concepts tailored to the Erlang virtual machine (BEAM).[3] The project transitioned to substantive work in March 2008, when the first source files were created by Robert Virding, co-creator of Erlang. This laid the foundation for LFE as a Lisp dialect interoperable with Erlang/OTP. The initial public release arrived later in 2008, introducing version 0.1 with basic compilation to Erlang bytecode and limited feature support. Development proceeded steadily through the 2010s, incorporating integration with successive Erlang/OTP releases to ensure runtime compatibility. Community contributions emerged around 2010, facilitated by the project's migration to GitHub, which enabled collaborative enhancements.[1] A key milestone was achieved by 2012, when LFE gained full compatibility with Core Erlang, allowing seamless generation of intermediate code for the BEAM compiler.[10] After eight years of iterative refinement, LFE reached version 1.0 in March 2016, delivering a stable, production-ready implementation with robust Lisp-2 semantics and full OTP interoperability.[8] Version 2.0 followed in June 2021, introducing improved macro expansion, enhanced REPL functionality, and better support for modern Erlang features.[11] Recent efforts have emphasized maintenance releases from 2023 to 2025, prioritizing BEAM VM compatibility—including support for OTP 27—along with bug fixes and expansions to the ecosystem libraries, without introducing major paradigm shifts. The latest version, 2.2.0, was released on January 11, 2025.[11][12]Overview and features
Design goals
Lisp Flavored Erlang (LFE) was designed to bridge the expressive power of Lisp with the robust concurrency and fault-tolerance model of Erlang, enabling developers to leverage Lisp traditions on a production-ready runtime without compromising performance.[2][7] A core objective is achieving 100% compatibility with Core Erlang, allowing LFE code to compile directly to the BEAM virtual machine and interoperate seamlessly with Erlang and OTP libraries.[1][7] This ensures that LFE applications can execute on the Erlang VM with the same guarantees for soft real-time, distributed, and fault-tolerant systems that power high-availability infrastructure worldwide.[2] The language prioritizes simplicity in its core design, featuring a minimal syntax that serves as a front-end to the Erlang compiler while supporting extensions through Lisp-style macros.[1][2] This approach facilitates metaprogramming capabilities, such as homoiconicity and low-hygiene macros, allowing for dynamic code generation and domain-specific languages directly within the REPL or runtime.[2] By providing Lisp syntax for Erlang's actor-based concurrency model—using lightweight processes and message passing—LFE enables the construction of scalable, non-stop applications without the overhead of traditional Lisp implementations.[7][2] Unlike many Lisps that adopt a Lisp-1 namespace model (unifying function and variable namespaces), LFE employs a Lisp-2 model with separate namespaces to align precisely with Erlang's semantics, avoiding conflicts in function application and variable binding.[7] This design choice, informed by the constraints and strengths of the Erlang VM, ensures that LFE remains a "proper Lisp" tailored for building distributed systems rather than a general-purpose dialect detached from its runtime.[7]Notable features
LFE exhibits homoiconicity, a core Lisp trait where code is represented as data structures, facilitating metaprogramming and the creation of domain-specific languages through seamless manipulation of s-expressions.[13] The language provides unhygienic macros enhanced with pattern matching support, enabling developers to extend the language syntax and generate code dynamically while capturing lexical bindings from the surrounding context.[14][15] LFE features a robust REPL that supports interactive development, including the definition of functions and macros directly in the environment, as well as script execution and evaluation of arbitrary expressions on the BEAM virtual machine.[2] Tail-call optimization is fully supported, inherited from the underlying Erlang runtime, allowing recursive functions to execute without stack growth and promoting efficient higher-order functional programming patterns. Pattern matching and guards are integrated into function definitions, providing declarative mechanisms for destructuring data and applying conditional logic in a concise manner.[13] Running on the BEAM VM, LFE benefits from per-process garbage collection, which operates independently for each lightweight process to prevent system-wide pauses and ensure suitability for soft real-time applications.[16]Syntax
Basic syntax elements
Lisp Flavored Erlang (LFE) employs S-expressions as its fundamental syntactic structure, inheriting from Lisp traditions where code and data are represented uniformly as nested lists enclosed in parentheses.[15] An S-expression can be a simple literal or a compound form consisting of a head element followed by zero or more arguments, such as(add 1 2), which denotes a function call to add with arguments 1 and 2.[17] This prefix notation facilitates homoiconicity, allowing programs to manipulate their own structure as data.[4]
Basic literals in LFE include atoms, numbers, and strings. Atoms are symbolic constants, typically starting with a lowercase letter or consisting of special characters, like hello or ok, and evaluate to themselves without quotes.[17] Numbers encompass integers, such as 42 or binary/hex notations like #b101 and #xFF, as well as floating-point values like 3.14.[18] Strings are delimited by double quotes, for example "LFE", and binaries use the #"" prefix for efficient byte sequences, such as #"binary".[4]
Lists in LFE are constructed as proper or improper linked structures using cons cells, represented syntactically within parentheses. A proper list like (1 2 3) is equivalent to (cons 1 (cons 2 (cons 3 '()))), where '() denotes the empty list and cons builds pairs.[17] Improper lists terminate in a non-list value, such as (1 2 . 3), created via (cons 1 (cons 2 3)).[15] The list function provides a convenient constructor, e.g., (list 1 2 3).[4]
Modules in LFE are defined using the defmodule macro, which encapsulates functions and other definitions within a namespace, supporting LFE's Lisp-2 semantics where function and variable bindings are resolved separately.[17] For instance, (defmodule my-module ...) declares a module named my-module. Functions within modules are defined with defun, specifying the name, argument list, and body, such as (defun square (x) (* x x)).[15] Exports are specified via (export (function-name arity) ...).[4]
Comments in LFE are introduced with a semicolon for single-line remarks, e.g., ; This is a comment, and block comments use #| ... |# delimiters for multi-line text.[18] Quoting prevents evaluation of an expression, using the quote operator ' or the quote function, as in ' (1 2 3) to denote a literal list rather than a function call.[17]
Expressions and operators
In LFE, expressions are primarily written in prefix notation, a hallmark of Lisp dialects, where the operator or function name precedes its arguments enclosed in parentheses. For instance, the arithmetic expression(+ 1 2) evaluates to 3 by applying the addition operator to the literals 1 and 2.[18] This notation extends to all function calls and built-in operations, ensuring uniform evaluation where arguments are fully evaluated before the operator is applied.[19]
Special forms provide control flow without full argument evaluation, distinguishing them from regular functions. The if form, for example, takes a predicate and two expressions: (if <predicate> <consequent> <alternate>), evaluating the consequent if the predicate evaluates to anything other than the atom 'false' and the alternate otherwise; note that the empty list '()' is truthy; the alternate is optional and defaults to nil.[20] An example is (if (=:= 1 1) "equal" "not equal"), which returns "equal". The cond form handles multiple conditional branches: (cond (<p1> <e1>) ... (<pn> <en>)), evaluating the first expression <ei> whose predicate <pi> evaluates to non-false, or nil if none match; predicates can include pattern matching with (?= pat expr).[21] For sequencing expressions, progn evaluates its arguments in order and returns the last: (progn expr1 expr2 ... exprn).[18]
Operators in LFE are functions that support multiple arguments and follow strict left-to-right evaluation for arguments of the same precedence level. Arithmetic operators include + (addition), - (subtraction), * (multiplication), and / (division), all of which evaluate all operands before applying the operation pairwise from left to right; for example, (+ 1 2 3) yields 6, equivalent to (+ 1 (+ 2 3)).[19] Comparison operators such as == (exact equality), =/= (inequality), =< (less than or equal), < , > , and >= also accept multiple arguments, returning true if the relation holds across all pairs; (>= 5 3 1) returns true.[19] Logical operators and , or , and not operate strictly on boolean atoms ('true' or 'false'), throwing 'badarg' on non-booleans; (and true false) returns false , (or false true) returns true , and (not false) returns true ; however, unlike short-circuiting variants andalso and orelse , these evaluate all arguments.[22]
Anonymous functions are defined using the lambda special form: (lambda (param1 param2 ...) body), which creates a closure capturing the current environment; for example, (lambda (x) (+ x 1)) defines a function incrementing its argument.[18] Pattern-matching variants use match-lambda : (match-lambda (((list a b) ...) (+ a b))) for destructuring lists. Operator precedence is governed by parenthetical grouping, with special forms parsed unevaluated until complete, and same-precedence operators associating left-to-right within their scope.[18]
Semantics
Functional aspects
LFE adheres to a functional model in its core semantics, where data structures are immutable, ensuring that once created, values cannot be modified in place. This design choice, inherited from the underlying Erlang virtual machine (BEAM), promotes predictability and facilitates reasoning about code behavior by eliminating unexpected mutations. Core functions are encouraged to avoid side effects, such as direct I/O or state changes, fostering referential transparency—where expressions can be substituted with their values without altering program semantics.[7] Functions in LFE are defined using thedefun macro, which supports multiple clauses with pattern matching on arguments for concise and expressive definitions. For instance, the factorial function can be implemented recursively as follows:
(defun factorial (0)
1)
(defun factorial (n)
(* n ([factorial](/page/Factorial) (- n 1))))
This allows the function to handle base cases and recursive steps through pattern matching on the input n, evaluating arguments strictly before applying the function body.[23]
LFE supports higher-order functions, enabling functions to be passed as arguments, returned as results, or stored in data structures, which enhances abstraction and code reuse. Built-in functions like apply dynamically invoke other functions with given arguments, while list-processing utilities such as lists:map and lists:foldl (for reduction) apply a provided function across collections. Lambdas, defined with (lambda (args) body), create anonymous functions that support closures by capturing their lexical environment. An example using lists:map to square a list of numbers is:
(lists:map (lambda (x) (* x x)) '(1 2 3))
;; => '(1 4 9)
This yields a new list with transformed elements, preserving immutability.[24]
Recursion serves as the primary mechanism for iteration in LFE, with the language providing automatic tail call optimization (TCO) via the BEAM VM to prevent stack overflows in deep recursive calls. In a tail-recursive formulation, the recursive invocation appears as the final expression in the function body, allowing the compiler to reuse the current stack frame. For example, a tail-recursive sum function accumulates results explicitly:
(defun sum (l)
(sum-acc l 0))
(defun sum-acc ('() total)
total)
(defun sum-acc ((cons h t) total)
(sum-acc t (+ h total)))
This approach ensures constant stack usage regardless of list length, aligning with functional principles of efficiency and purity.[25]
Concurrency model
LFE's concurrency model is based on the actor model, where computation is performed by lightweight processes that communicate exclusively through asynchronous message passing, eschewing shared memory to ensure isolation and scalability.[26] This approach allows for massive concurrency, with systems capable of supporting millions of processes on commodity hardware, as demonstrated in early benchmarks handling over 20 million processes on a single machine.[26] Processes in LFE are created using thespawn built-in function, which launches an independent actor with its own isolated heap and state, preventing race conditions inherent in shared-memory models. For instance, (spawn 'module 'function '(arg1 arg2)) initiates a new process executing the specified exported function with arguments, returning a unique process identifier (PID) such as <0.123.0>.[26] Each process maintains private memory, and the runtime scheduler interleaves their execution transparently, enabling efficient handling of concurrent tasks without explicit threading.[26]
Message passing forms the core of inter-process communication, employing the asynchronous ! operator for sending and the receive form for synchronous reception with pattern matching. Senders use (! pid message) to dispatch data, such as (let* ((pid (spawn 'example 'loop '())) (msg "hello")) (! pid (tuple 'msg msg))), where the message is queued in the recipient's inbox without blocking.[27] Receivers employ (receive (pattern (when guard action) ...) (after timeout default-action)) to selectively extract matching messages, with the after clause providing timeouts for non-blocking operation; for example, (receive ((tuple 'msg text) (: io format "Received: ~s~n" (list text))) (after 5000 (: io format "Timeout~n" (list)))) waits up to 5 seconds before handling absence.[23] This pattern supports both fire-and-forget sends and request-reply interactions, leveraging LFE's functional purity to compose reliable concurrent behaviors.[23]
For supervision and fault-tolerance, LFE integrates the Open Telecom Platform (OTP) framework, adapting Erlang behaviors like gen_server and supervisors to Lisp syntax for building robust, hierarchical process trees. A gen_server in LFE is implemented via a single module defining callbacks such as init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, and code_change/3, enabling stateful servers with built-in error recovery; for example, handle_info/2 processes unexpected messages like exits via pattern matching on (tuple 'EXIT pid reason).[28] Supervisors monitor child processes using strategies like one-for-one or one-for-all restarts, specified in an init/1 return value such as (tuple 'ok (list (tuple 'child-module :start_link '()) (tuple :permanent 5000 :worker 'child-module))), ensuring system resilience through linked process crashes and automated recovery.[28]
Distribution in LFE enables transparent clustering across nodes, facilitating fault-tolerant, scalable systems without altering application code. Nodes connect via a shared magic cookie in .erlang.cookie files and are named with lfe -sname node@host, allowing processes to send messages or spawn remotely using PIDs or registered names, as in (! (tuple 'remote@host 'ping) 'hello) or (spawn 'remote@host 'module 'function 'args).[29] This location-transparent model extends the actor paradigm over networks, supporting hot code upgrades and seamless failover in distributed environments.[29]
Erlang compatibility
Interoperability mechanisms
Lisp Flavored Erlang (LFE) achieves full compatibility with Erlang code and the BEAM virtual machine ecosystem through its compiler, which translates LFE source code into Core Erlang, an intermediate representation used by the Erlang compiler.[15] This process ensures that LFE modules produce the same bytecode as equivalent Erlang modules, allowing binary-level interoperability without requiring separate runtimes or loaders.[30] The compilation involves three main passes: macro expansion to handle LFE-specific Lisp features, linting for semantic checks, and code generation that maps LFE core forms—such ascase, lambda, and try—directly to Core Erlang constructs.[15]
In terms of syntax translation, the LFE compiler processes S-expressions and alternative bracket notations into Core Erlang equivalents, preserving the semantics while adapting Lisp-style syntax to Erlang's functional model.[23] For instance, LFE symbols are treated as Erlang atoms, tuples are constructed with #(...) notation mapping to Erlang tuples, and binaries use #b(...) to align with Erlang's binary syntax.[15] This translation enables LFE code to be mixed arbitrarily with Erlang code in the same application, as both compile to identical BEAM bytecode.[2]
Calling conventions between LFE and Erlang modules are straightforward and direct, using the (call Module Function [Arity](/page/Arity)) form to invoke any Erlang function by its module, name, and arity.[23] Local LFE functions can be called similarly with (funcall Function [Arity](/page/Arity)), and Erlang modules can import and export functions to LFE without modification, supporting seamless bidirectional calls.[31] This mechanism allows LFE developers to leverage existing Erlang libraries, such as OTP behaviors, by simply referencing them in LFE code.[2]
Data interchange between LFE and Erlang relies on their shared runtime types, eliminating the need for conversions or marshalling.[30] Both languages use identical representations for core data structures, including atoms (as symbols in LFE), lists, tuples, binaries, and maps, ensuring that values passed between modules retain their structure and behavior on the BEAM.[15] For example, an LFE-constructed tuple can be directly consumed by an Erlang function without alteration.[32]
Access to Erlang's built-in functions (BIFs) is seamless in LFE, as they are callable like native Lisp functions or via explicit module calls, such as (erlang:list_to_atom "foo").[23] LFE modules can also load and utilize Native Implemented Functions (NIFs) in the same manner as Erlang modules, by linking C-based implementations at load time and invoking them through standard function calls, thereby extending performance-critical operations across the ecosystem.[33] This integration allows LFE to benefit from the full suite of BEAM optimizations and native extensions without additional wrappers.[2]
Adapting Erlang constructs
LFE adapts Erlang's core constructs by embedding them within Lisp's s-expression syntax, enabling developers to leverage Erlang's robust pattern matching, guards, and concurrency primitives while benefiting from Lisp's homoiconicity and macro system. This integration allows LFE code to compile directly to Core Erlang, ensuring seamless compatibility with the BEAM virtual machine, but reimagines Erlang idioms through parenthesized forms and symbolic representations.[34][35] Pattern matching in LFE, drawn from Erlang, performs unification to bind variables or extract structure from data, but uses Lisp-style lists and quoted symbols instead of Erlang's literal syntax. It appears in function heads,let bindings, case expressions, and receive blocks, where patterns are written as ordinary data expressions with unbound symbols acting as variables. For instance, to extract the head of a cons cell, a function clause might be defined as (defun head ((cons h t)) h), which matches a list structure and binds h to the first element via unification, failing if the input is not a cons.[34][35] Variables are bound only upon successful match, and the underscore _ serves as an anonymous variable that matches without binding. This approach preserves Erlang's declarative power for destructuring tuples, lists, and binaries, but aligns with Lisp's prefix notation for more fluid nesting.[34]
Guards extend pattern matching in LFE by adding boolean conditions to function clauses and other dispatch sites, using the when form to filter matches based on predicates like type checks or arithmetic. In function heads, a guard follows the pattern, such as (defun even? (n when (and (is_integer n) (== (rem n 2) 0))) true), which only applies if n is an integer and even, otherwise skipping to subsequent clauses.[21] Guards support Erlang's built-in functions (BIFs) for tests like is_atom, >, and ==, ensuring safe, compile-time verifiable branching without side effects. This Lisp-ified version of Erlang guards integrates seamlessly with pattern matching, for example in case as ((list a _) (when (is_atom a)) "Atom prefix"), providing conditional dispatch in a concise, parenthesized form.[21]
List comprehensions in LFE, adapted from Erlang's list and binary generators, use the lc macro to iterate, filter, and transform collections with Lisp syntax, supporting guards for refined control. The form (lc ((<- x lst) (when (even? x))) x) generates a new list of even elements from lst, where <- binds generator variables and when applies filters akin to Erlang's but with explicit guard support.[36] Qualifiers can mix generators, matches via (?= pat guard expr), and tests, enabling operations like mapping or flattening while compiling to efficient Erlang bytecode. Binary comprehensions follow similarly with bc and <= for bit-level extraction, but halt on guard failures unlike pure Erlang variants.[36]
Records in LFE provide structured data via defrecord, creating tagged tuples with accessor macros in a Lisp-friendly manner, mirroring Erlang records but defined as (defrecord person (name address age)), which generates constructors like make-person and field getters/setters. These are used for consistent data representation across modules, often placed in header files for sharing, and integrate with pattern matching for extraction, such as matching (person Name Age) in clauses.[37][38] Message receiving adapts Erlang's receive primitive using pattern-matched clauses, expressed as (receive ((tuple 'ok item) (process item)) ((tuple 'error reason) (handle-error reason))), where incoming messages are unified against patterns in a queue, blocking until a match with timeout options. This Lisp syntax unifies message handling with general pattern matching, facilitating concurrent programming idioms like selective reception.[35]
Examples
Simple functional programs
LFE's functional programming capabilities shine in simple, sequential programs that emphasize immutability, recursion, and higher-order functions for data transformation. These programs leverage the language's Lisp syntax for concise expression while drawing on the Erlang standard library for robust list handling.[18] A fundamental example is the recursive implementation of the factorial function, which uses pattern matching to handle the base case and recursive call. The function is defined as follows:(defun fac
((1) 1)
((n) (* n (fac (- n 1)))))
This matches the input 1 to return 1, and for any other positive integer n, multiplies n by the factorial of n-1. Calling (fac 5) evaluates to 120 through successive reductions: 5 * (4 * (3 * (2 * (1 * 1)))).[39]
List processing in LFE commonly employs higher-order functions from the lists module, combined with lambda expressions for custom transformations. The map function applies a given function to each element of a list, producing a new list. For instance, to square each number in a list:
(lists:map (lambda (x) (* x x)) '(1 2 3 4))
This yields '(1 4 9 16), where the lambda (lambda (x) (* x x)) defines an anonymous function that computes the square. Similarly, the filter function selects elements satisfying a predicate:
(lists:filter (lambda (x) (> x 0)) '(1 -2 3 -4 5))
This returns '(1 3 5), retaining only positive integers. Lambdas enable these operations to remain flexible and composable without defining named functions for one-off uses.[18]
Control flow in simple LFE programs often relies on the cond form for multi-way branching, which evaluates clauses sequentially until a true condition is found. Each clause consists of a test expression followed by a body expression, with an optional true clause as the default:
(defun classify (x)
(cond ((> x 0) 'positive)
((= x 0) 'zero)
(true 'negative)))
Invoking (classify 3) returns positive, while (classify -1) returns negative. This structure supports clear, readable decision logic without imperative loops.[21]
For a practical module demonstrating string manipulation—treating strings as lists of characters—consider a module that reverses a list. The following complete module exports both a wrapper using the built-in lists:reverse and a recursive implementation for educational purposes:
(defmodule string-utils
(export (reverse-list 1)
(reverse-rec 2)))
(defun reverse-list (lst)
"Reverse a list using the [standard library](/page/Standard_library)."
(lists:reverse lst))
(defun reverse-rec
(('() acc) acc)
(((cons head tail) acc)
(reverse-rec tail ([cons](/page/Cons) head acc))))
To reverse the list '(a b c), call (string-utils:reverse-rec '(a b c) '()), which returns '(c b a). For a string like "hello", first convert to a list with (string:to_list "hello"), then apply the function to get '(o l l e h). This recursive approach builds the reversed list by accumulating elements in reverse order, illustrating tail recursion for efficiency on the Erlang VM.[32][39]
Concurrent and distributed examples
LFE leverages the Erlang concurrency model, enabling lightweight processes that communicate via asynchronous message passing. A basic example of concurrency in LFE involves spawning a process and exchanging messages, such as in a simple ping-pong scenario. The following code defines aping function that sends a fixed number of messages to a pong process and receives responses, while the pong function loops to handle incoming messages and reply accordingly.
(defmodule ping-pong
(export (start-ping 1) (start-pong 0) (ping 2) (pong 0)))
(defun ping
((0 _)
(lfe_io:format "Ping finished~n" ()))
((n pong-pid)
(! pong-pid `#(ping ,(self)))
(receive
(pong (lfe_io:format "Ping received pong~n" ())))
(ping (- n 1) pong-pid)))
(defun pong ()
(receive
(`#(ping ping-pid)
(lfe_io:format "Pong received ping~n" ())
(! ping-pid 'pong)
(pong))))
(defun start-pong ()
(spawn 'ping-pong 'pong ()))
(defun start-ping (pong-pid)
(spawn 'ping-pong 'ping `(,3 ,pong-pid)))
To execute this, first spawn the pong process with (start-pong), obtain its PID, then spawn the ping process with (start-ping pong-pid). This demonstrates process spawning using spawn and message passing with ! (send) and receive, where each process runs independently and concurrently.
For stateful concurrency, LFE supports the OTP gen_server behavior, which provides a framework for supervising processes with callbacks for initialization, message handling, and termination. The gen_server enables reliable, fault-tolerant servers by managing state through synchronous calls and asynchronous casts. A minimal counter server example splits the implementation into a callback module for handling messages and a server module for starting the process and providing an API.[40]
In the callback module:
(defmodule counter-callback
(export all))
(defun init (initial-state)
`#(ok ,initial-state))
(defun handle_cast
(('increment state-data)
`#(noreply ,(+ 1 state-data)))
(('decrement state-data)
`#(noreply ,(- state-data 1))))
(defun handle_call
(('get-amount _from state-data)
`#(reply ,state-data ,state-data))
((unknown _from state-data)
`#(reply #(error "Unknown command") ,state-data)))
(defun terminate (_reason _state-data)
'ok)
In the server module:
(defmodule counter-server
(behaviour gen_server)
(export all))
(defun server-name () 'counter)
(defun callback-module () 'counter-callback)
(defun initial-state () 0)
(defun start-link ()
(gen_server:start-link `#(local , (server-name))
(callback-module)
(initial-state)
()))
(defun increment ()
(gen_server:cast (server-name) 'increment))
(defun decrement ()
(gen_server:cast (server-name) 'decrement))
(defun get-amount ()
(gen_server:call (server-name) 'get-amount))
Start the server with (counter-server:start-link), then use increment, decrement, and get-amount to interact concurrently. The handle_cast callbacks update state asynchronously without blocking, while handle_call provides synchronous queries, ensuring thread-safe state management across multiple clients.[40]
LFE's interoperability with Erlang allows seamless calls to Erlang modules from LFE code and vice versa, as both compile to BEAM bytecode. For instance, an LFE function can invoke erlang:display to output a term directly. Consider this simple logging function:
(defmodule logger
(export (log 1)))
(defun log (term)
(erlang:display term))
Calling (logger:log "Hello from LFE") displays the string using Erlang's built-in function. Conversely, from an Erlang shell or module, an LFE-compiled module like logger can be loaded with lfe:load("logger") and invoked as logger:log("Hello from Erlang"), demonstrating bidirectional compatibility without wrappers.[18]
For distributed computing, LFE supports node clustering similar to Erlang, using named nodes for remote process spawning and message passing. Extending the ping-pong example to multiple nodes requires shared authentication via an .erlang.cookie file and starting nodes with -sname. Assume two nodes: ping@host1 and pong@host2. On the pong node, register the process as before but use the node name in messages.[29]
Modified start functions:
(defun start-pong ()
(register 'pong (spawn 'ping-pong 'pong ())))
(defun start-ping (pong-node)
(spawn 'ping-pong 'ping `(,3 ,pong-node)))
Start the pong node: lfe -sname pong@host2, run (start-pong). On the ping node: lfe -sname ping@host1, then (start-ping '#(pong host2)). The ping process sends messages to the remote pong via the tuple #(registered-name node), enabling distribution across clustered nodes for scalable, fault-tolerant systems. Node linking can be added using net_adm:ping/1 to connect nodes dynamically before spawning.[29]