Skip to content

LISP - Functions vs Macros

Understanding the difference between functions and macros is fundamental to mastering Common Lisp. While they look similar, they operate at different stages of program execution. Functions work with data at runtime, while macros transform code at compile time. This metaprogramming capability is what makes Lisp so extensible.

A function is the standard building block for computation in most languages.

  • Evaluation: Arguments are evaluated first, then their resulting values are passed to the function body.
  • Operates On: Values (numbers, strings, lists, etc.).
  • Purpose: To perform a computation and return a value.
  • Analogy: A chef following a recipe. The ingredients (arguments) are measured and prepared before the cooking (function execution) begins.

A macro is a piece of code that writes other code.

  • Evaluation: A macro runs at compile time. It receives the literal code passed to it as arguments.
  • Operates On: Code (S-expressions).
  • Purpose: To transform a piece of code into another piece of code, which is then compiled and executed.
  • Analogy: A recipe writer. Given a request like “a simple pasta dish,” they write a new, specific recipe (the macro expansion) which the chef will then follow.

The Decisive Difference: Control Over Evaluation

Section titled “The Decisive Difference: Control Over Evaluation”

The most critical difference is that macros control if, when, and how many times their arguments are evaluated. Functions have no such control—their arguments are always evaluated exactly once, before the function is called.

Let’s try to implement our own if statement. With a function, this is impossible:

;; The functional approach FAILS
(defun my-if-function (condition then-form else-form)
(if condition then-form else-form))
;; This call will ERROR!
;; Lisp evaluates ALL arguments *before* calling my-if-function.
;; So, (error "I am always called!") runs no matter what.
(my-if-function t (print "Success!") (error "I am always called!"))

A macro, however, can solve this because it can generate code that only includes the branch that should be executed.

;; The macro approach WORKS
(defmacro my-if-macro (condition then-form else-form)
;; The backquote ` creates a code template.
;; The comma , evaluates and splices in the value.
`(if ,condition
,then-form
,else-form))
;; This call works perfectly!
;; The macro expands into `(if t (print "Success!") (error "...not included"))`
;; The error form is never part of the final code, so it is never evaluated.
(my-if-macro t (print "Success!") (error "I am never called!"))

You can use macroexpand-1 to see what code a macro generates:

CL-USER> (macroexpand-1 '(my-if-macro t (print "Success!") (error "I am never called!")))
(IF T (PRINT "Success!") (ERROR "I am never called!")) ; The expanded code
T ; Indicates it was a macro expansion

“If you can write it as a function, write it as a function.”

Macros are more complex and can introduce subtle bugs (like evaluating an argument multiple times). Reserve them for situations where they are truly necessary:

  • 1. Creating new control structures: When you need to control the evaluation of arguments (e.g., if, when, unless, loop).
  • 2. Reducing boilerplate code: When you have a repetitive code pattern that can be abstracted away at compile time (e.g., with-open-file).
  • 3. Building Domain-Specific Languages (DSLs): Creating a new, specialized syntax within Lisp for a particular problem domain (e.g., an HTML generation library).
FeatureFunctionMacro
Evaluation TimeRuntimeCompile Time
Argument HandlingEvaluates arguments before executionReceives unevaluated code (S-expressions) as arguments
Primary PurposeCompute with dataTransform and generate code
Key TakeawayUse for all standard computationsUse to extend Lisp’s syntax and control evaluation