LFE Style Guide - Consolidated Reference
Quick Reference for AI Coding Tools
This guide consolidates LFE style conventions in order of immediate applicability: syntax → semantics → architecture.
1. SYNTAX & FORMATTING
Indentation
Standard: 2 spaces per form
(defun f ()
(let ((x 1)
(y 2))
(lfe_io:format "X=~p, Y=~p~n" (list x y))))
Exception: Pattern matching and conditionals
;; cond - align clauses
(cond ((lists:member x '(1 2 3)) "First three")
((=:= x 4) "Is four")
((>= x 5) "More than four")
('true "You chose poorly"))
;; defun with pattern matching - compact related clauses
(defun ackermann
((0 n) (+ n 1))
((m 0) (ackermann (- m 1) 1))
((m n) (ackermann (- m 1) (ackermann m (- n 1)))))
;; Multi-line arguments
(do-something first-argument
second-argument
(lambda (x) (frob x))
fourth-argument
last-argument)
Use Emacs LFE mode for consistency:
;; Add to ~/.emacs
(setq-default indent-tabs-mode nil)
(defvar lfe-dir (concat (getenv "HOME") "/git/lfe/emacs"))
(setq load-path (cons lfe-dir load-path))
(require 'lfe-start)
Whitespace
Vertical spacing:
- One blank line between top-level forms
- Exception: Related simple definitions can be grouped without blank lines
(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 spacing:
- No extra whitespace around parentheses
- One space between forms
- All closing parens on the same line
- Do NOT vertically align middle-of-line forms
;; BAD
(let* ((low 1)
(high 2)
(sum (+ (* low low) (* high high))))
...)
;; GOOD
(let* ((low 1)
(high 2)
(sum (+ (* low low) (* high high))))
...)
Line Length
Maximum 80 characters per line
Reasons:
- Web display (paste-bins, gists, documentation)
- Multiple editor panes side-by-side
- Emergency terminal access
- Encourages good naming discipline
File Headers
;;;; Brief description of file contents.
(defmodule module-name
...)
Copyright only needed if different from project LICENSE file.
2. NAMING CONVENTIONS
Symbols (Atoms)
Always lowercase with hyphens
;; BAD
*default-username*
*max-widget-cnt*
;; GOOD
*default-user-name*
*maximum-widget-count*
Rules:
- Always use
-between words (never/or.unless well-documented reason) - Must be pronounceable
- No overly short names except in tiny scopes
Predicates (Boolean Functions)
Choose ONE convention per project:
trailing?(modern Lisps) - RECOMMENDED for LFEtrailing-p(multi-word classic Lisp)trailingp(single-word classic Lisp)is-leading(Erlang style)
;; Consistent usage - pick one style
(defun prime? (n) ...)
(defun even? (n) ...)
;; OR (not both)
(defun is-prime (n) ...)
(defun is-even (n) ...)
Constants and Defaults
;; Constants: + earmuffs +
(defun +my-pi+ () 3.14)
;; Defaults/config: * earmuffs *
(defun *default-host* () "127.0.0.1")
Module-Relative Names
Do NOT repeat module name in symbols
;; BAD
(defmodule varint
(export (varint-length64 0)))
(defun varint-length64 () ...)
;; Usage becomes ugly:
(varint:varint-length64)
;; GOOD
(defmodule varint
(export (length64 0)))
(defun length64 () ...)
;; Usage is clean:
(varint:length64)
Intent Over Content
Name by concept (intent), not implementation (content)
;; BAD - names reveal implementation
user-list
row-array
config-hash-table
;; GOOD - names reveal meaning
users
active-row
configuration
Exception: Generic algorithms operating on arbitrary data structures can use type names.
3. DOCUMENTATION
Comment Levels
Four levels by semicolon count:
;;;; File headers and large section comments
(defmodule math-n-things
(export (utility-function 0)))
;;; Section headers for groups of related functions
;;; Can include multi-line explanatory text
(defun utility-function ()
;; Comment applying to following code section
(do-something)
(do-something-else) ; Parenthetical remark
(final-thing)) ; Aligned remarks
Rules:
- Always space after semicolons
- Align single-semicolon remarks vertically when consecutive
Docstrings
Document ALL visible functions
(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)))))))
Format:
- First line: concise summary
- Optional: detailed explanation after blank line
- Continuation lines indented 2 spaces (align with opening quote)
- Describe contract: inputs, outputs, side effects, conditions
When forms don't support docstrings, use comments:
;;; This record tracks test results for reporting.
(defrecord state
(status (orddict:new))
test-type
(ok 0)
(fail 0))
TODO and XXX Comments
;; --- TODO (alice@gmail.com): Refactor to provide better API.
;; --- XXX (bob): Critical bug causing cascading failures.
;; See: https://github.com/project/issues/42
Conventions:
TODO: Normal tasks, incomplete featuresXXX: Bugs, potential issues, inelegance, uncertainty- Synonyms:
BUG,FIXME,HACK
- Synonyms:
- Include identifier (username/email)
- Reference issue tracker when applicable
4. MODULE STRUCTURE
Module Definition
Export functions on separate lines, alphabetically sorted
;; NEVER use this
(defmodule maths
(export all)) ;; NEVER DO THIS
;; BAD - wrong order, inconsistent arity grouping
(defmodule maths
(export (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)
(ackermann 2)
(factorial 1)))
;; GOOD
(defmodule maths
(export
(ackermann 2)
(factorial 1) (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)))
;; GOOD - separate exports for logical grouping
(defmodule maths
(export
(util-func 1)
(other-util 2))
(export
(ackermann 2)
(factorial 1) (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)))
Pseudo-Packages (Sub-directories)
;; Module in src/project/subdir/maths.lfe
(defmodule project.subdir.maths
(export
(factorial 1)))
;; Client usage - import for readability
(defmodule client
(export (some-func 0))
(import
(from project.subdir.maths
(factorial 1))))
(defun some-func ()
(factorial 5))
;; OR rename to avoid collision
(defmodule client
(export (some-func 0))
(import
(rename project.subdir.maths
((factorial 1) fact))))
(defun some-func ()
(fact 5))
When to Create Modules
Create separate modules for:
- Reusable functionality
- Clear separation of concerns
- Code that needs independent testing
Workflow:
- Start small, stay focused
- Write only needed functions
- Keep functions small (one task each)
- Make incremental changes
For new code:
- Prototype in REPL
- Paste working code into test module
- Move final version to src module
- Verify tests pass
5. FUNCTIONS
Keep Functions Small
One function = one task
If doing six things, create six functions.
Group Functions Logically
;;;; Exported Functions
(defun public-api-1 () ...)
(defun public-api-2 () ...)
;;;; Internal Functions
(defun helper-1 () ...)
(defun helper-2 () ...)
Generally put exported functions first, unless readability benefits from different ordering.
Refactor Complex Conditionals
Extract complex conditions into named predicates
;; BAD - inline complexity
(if (and (fuelled? rocket)
(lists:all #'strapped-in? (crew rocket))
(sensors-working? rocket))
(launch rocket)
(! pid `#(err "Aborting launch.")))
;; GOOD - named abstraction
(defun rocket-ready? (rocket)
(and (fuelled? rocket)
(lists:all #'strapped-in? (crew rocket))
(sensors-working? rocket)))
(if (rocket-ready? rocket)
(launch rocket)
(! pid `#(err "Aborting launch.")))
Pattern Match in Function Heads
Don't write complex case statements with deep nesting
Split into functions using pattern matching in heads.
6. DATA STRUCTURES
Lists
Appropriate access patterns:
;; Simple access
(car lst)
(cdr lst)
(cadr lst)
;; Pattern matching
(let ((`(,head . ,tail) lst))
...)
;; Erlang lists module
(lists:nth 1 lst)
(lists:reverse lst)
Avoid using lists as ad-hoc structures
- Don't use lists to pass multiple heterogeneous values
- Exception: function argument lists, apply arguments
- Use records for heterogeneous collections
Maps
Do NOT align keys and values
;; BAD
'#m(k1 v1
key2 value2
key-the-third value-the-third)
;; GOOD
#m(k1 v1
key2 value2
key-the-third value-the-third
another one)
Records
Primary data structure for messages and complex data
;; Define in header (.lfe) if used across modules
;; Define in module if used only locally
(defrecord person
name
age
occupation)
;; ALWAYS use record macros - NEVER match tuples directly
;; BAD
(let ((`#(person ,name ,age) joe))
...)
;; GOOD
(let* ((joe (make-person name "Joe" age 29))
(name (person-name joe)))
...)
Benefits:
- Cross-module consistency
- Enforced by compiler
- Self-documenting code
Tuples and Proplists
Standard formatting (no alignment)
;; BAD
'(#(k1 v1)
#(key2 value2)
#(key-the-third value-the-third))
;; GOOD
'(#(k1 v1)
#(key2 value2)
#(key-the-third value-the-third)
#(another one))
7. PROCESSES AND CONCURRENCY
One Process Per Module
Code for a process's top loop stays in ONE module
- Process can call library functions elsewhere
- But control flow must be in single module
- Conversely: one module = one process type
Process-to-Activity Mapping
One parallel process per truly concurrent real-world activity
This creates natural, understandable structure.
Process Roles
Each process has ONE role:
- Client
- Server
- Supervisor (watches/restarts others)
- Worker (can have errors)
- Trusted Worker (must not error)
Don't mix roles in one process.
Registered Processes
Register with same name as module
(defmodule fileserver
...)
;; Register as 'fileserver
(register 'fileserver pid)
Only register long-lived processes.
Process Dictionary - Avoid
Do NOT use get/put unless absolutely necessary
;; BAD - uses process dictionary
(defun tokenize
((`(,head . ,tail)) ...)
(('())
(case (get-characters-from-device (get 'device))
('eof '())
(`#(value ,chars) (tokenize chars)))))
;; GOOD - explicit parameter
(defun tokenize
((device `(,head . ,tail)) ...)
((device '())
(case (get-characters-from-device device)
('eof '())
(`#(value ,chars) (tokenize device chars)))))
Process dictionary makes functions non-deterministic and harder to debug.
8. SERVERS
Use Generic Servers
Leverage OTP gen_server or similar libraries
Consistent use of generic servers simplifies system architecture.
Tail Recursion Required
ALL servers must be tail-recursive
;; BAD - not tail recursive, will consume memory
(defun loop ()
(receive
(`#(msg1 ,msg1)
...
(loop))
('stop 'true))
(io:format "Server going down" '())) ;; DON'T DO THIS
;; GOOD - tail recursive
(defun loop ()
(receive
(`#(msg1 ,msg1)
...
(loop))
('stop
(io:format "Server going down" '()))
(other
(logger:error "Unknown msg ~w~n" `(,other))
(loop))))
9. MESSAGES
Tag All Messages
Makes receive order irrelevant and extension easier
;; BAD - untagged messages
(defun loop (state)
(receive
(`#(,mod ,funcs ,args) ;; Ambiguous!
(erlang:apply mod funcs args)
(loop state))))
;; GOOD - tagged messages
(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
;; BAD - can't distinguish false result from not-found
(defun keysearch
((key `(#(,key ,value) . ,tail)) value)
((key `(#(,_ ,_) . ,tail)) (keysearch key tail))
((key '()) 'false))
;; GOOD - tagged return
(defun keysearch
((key `(#(,key ,value) . ,tail)) `#(value ,value))
((key `(#(,_ ,_) . ,tail)) (keysearch key tail))
((key '()) 'false))
Flush Unknown Messages
Always have catch-all in receive
(defun main-loop ()
(receive
(`#(msg1 ,msg1)
...
(main-loop))
(`#(msg2 ,msg2)
...
(main-loop))
(other
(logger:error "Process ~w got unknown msg ~w~n"
`(,(self) ,other))
(main-loop))))
Use Interface Functions
Encapsulate message passing behind functions
(defmodule fileserver
(export
(start 0)
(stop 0)
(open-file 1)))
(defun open-file (filename)
(! fileserver `#(open-file-request ,filename))
(receive
(`#(open-file-response ,result) result)))
Message protocol is internal, hide it from clients.
Timeouts
Be careful with after in receive
Must handle case where message arrives late.
Exit Trapping
Minimize processes that trap exits
Processes should either always trap or never trap. Don't toggle.
10. ERROR HANDLING
Separate Error and Normal Case
Don't clutter normal case with error handling
;; Program for the happy path
;; Let it crash on errors
;; Handle errors in separate supervisor process
Clean separation simplifies system design.
Identify Error Kernel
Determine what MUST be correct
Like OS kernel vs user programs:
- Error kernel must be correct
- Application code can fail without system compromise
- Often contains critical real-time state
11. LIBRARIES AND PROJECTS
Use Before You Write
Search for existing libraries first
- No-dependency projects aren't virtuous
- Doesn't aid portability or executable creation
- Check licensing compatibility
When to Write Alternative Library
Only if you provide significant value:
- Complete documentation
- Comprehensive examples
- Well-designed website/resources
Structure Projects as Library Collections
Many small libraries > one monolith
If you abandon 30% of a monolith, it's useless to others. If you abandon a collection of libraries, you've left useful components.
Project Structure with Pseudo-Packages
├── LICENSE
├── README.md
├── rebar.config
├── src
│ ├── title.app.src
│ └── title
│ ├── config.lfe
│ ├── db.lfe
│ ├── graphics
│ │ ├── mesh.lfe
│ │ ├── obj.lfe
│ │ └── gl.lfe
│ └── logging.lfe
└── test
Compiles to flat structure:
ebin/
title.app
title.config.beam
title.db.beam
title.graphics.mesh.beam
title.graphics.obj.beam
title.graphics.gl.beam
title.logging.beam
12. GENERAL PRINCIPLES
Team Development
Every developer must remember:
- "Hit by a truck" principle: Others must understand your code
- Consistency: All code should look the same
- Precision: Be exact
- Conciseness: Say more with less
- Simplicity: Use smallest hammer for job
- Common sense: Think before coding
- Cohesion: Keep related code together
Priority Order
When making decisions, optimize in this order:
- Usability by customer
- Debuggability/Testability
- Readability/Comprehensibility
- Extensibility/Modifiability
- Efficiency (runtime performance)
Efficiency Notes
- Choose performant option when complexity is equal
- Choose simpler option when complexity differs
- Profile before optimizing
- Avoid premature optimization
- Don't optimize rarely-run code
Architecture
For non-trivial changes:
- Write design document (at least 2 paragraphs)
- Get approval from affected parties
- Consider: reusability, component communication, evolution, team coordination
QUICK SYNTAX CHECKLIST
Before committing LFE code, verify:
- 2-space indentation (use Emacs LFE mode)
- No lines > 80 characters
- Lowercase atoms with hyphens (not underscores)
- Module names match filenames
- Exported functions listed alphabetically
- Docstrings on all public functions
- Comments use appropriate semicolon count
- No (export all)
- Records used for structured data
- Pattern matching in function heads, not case
- Tail-recursive server loops
- Tagged messages
- Interface functions hiding message protocols
- Unknown messages flushed in receive
- Complex conditionals extracted to named predicates
SPELLING CONVENTIONS
Correct spellings:
- complimentary (free meal) not complementary
- existent, nonexistent, existence (not -ant/-ance)
- hierarchy (not heirarchy)
- precede (not preceed)
- weird (not wierd)
Exceptions (industry standard):
- referer (HTTP header, not referrer)
Use aspell or similar for spell-checking.
SOURCES
This guide synthesizes conventions from:
- Google Common Lisp Style Guide
- Common Lisp Style Guide (lisp-lang.org)
- Peter Norvig & Kent Pitman's Lisp Style Tutorial
- Erlang Programming Rules and Conventions
- Inaka Erlang Guidelines
- Clojure Style Guide
- James Halliday's module philosophy
Remember: These are guidelines, not laws. Consistency within a project matters most. When contributing to existing projects, match their established style.
End of Consolidated LFE Style Guide