LISP - Functions vs Macros
Lisp’s Superpower: Functions vs. Macros
Section titled “Lisp’s Superpower: 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.
Functions: The Workhorses of Computation
Section titled “Functions: The Workhorses of Computation”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.
Macros: The Architects of Syntax
Section titled “Macros: The Architects of Syntax”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.
A Classic Demonstration
Section titled “A Classic Demonstration”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!"))Seeing the Expansion
Section titled “Seeing the Expansion”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 codeT ; Indicates it was a macro expansionThe Golden Rule of Macros
Section titled “The Golden Rule of Macros”“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).
Summary Table: Function vs. Macro
Section titled “Summary Table: Function vs. Macro”| Feature | Function | Macro |
|---|---|---|
| Evaluation Time | Runtime | Compile Time |
| Argument Handling | Evaluates arguments before execution | Receives unevaluated code (S-expressions) as arguments |
| Primary Purpose | Compute with data | Transform and generate code |
| Key Takeaway | Use for all standard computations | Use to extend Lisp’s syntax and control evaluation |