Building a Simple Calculator in LISP
Because arithmetic operators in LISP are just ordinary function calls written in prefix S-expression form, such as (+ 1 2) or (* 3 (+ 4 5)), building a calculator is one of the most natural first projects in the language: you are essentially writing a function that evaluates an already-parsed S-expression tree, which is close to what the LISP reader does internally when it reads your source code. This walkthrough builds a calculator that supports the four basic operators plus nested expressions and simple error handling for cases like division by zero.
Cricket analogy: Building a calculator in LISP is like scoring a match using a structured scorecard app where each entry (runs, wickets) already matches the app's internal data model, so displaying the final total requires almost no extra translation work.
Representing and Evaluating Expressions
A calculator expression like 3 + 4 * 2 can be represented directly as a LISP S-expression (+ 3 (* 4 2)), where the operator is the first element (the head) and the operands follow as the rest of the list. Evaluating this structure recursively is straightforward: if the expression is a number, return it as-is; otherwise, dispatch on the operator symbol at the head of the list, recursively evaluate each operand, and apply the corresponding arithmetic function—this recursive-descent pattern mirrors exactly how LISP's own eval function processes any S-expression.
Cricket analogy: Evaluating nested expressions recursively is like calculating a team's net run rate, where you first resolve each innings' sub-total before combining them into the final aggregate figure, working from the smallest unit outward.
;; A recursive evaluator for arithmetic S-expressions
(defun calc-eval (expr)
(cond
((numberp expr) expr)
((eq (first expr) '+) (+ (calc-eval (second expr)) (calc-eval (third expr))))
((eq (first expr) '-) (- (calc-eval (second expr)) (calc-eval (third expr))))
((eq (first expr) '*) (* (calc-eval (second expr)) (calc-eval (third expr))))
((eq (first expr) '/)
(let ((divisor (calc-eval (third expr))))
(if (zerop divisor)
(error "Division by zero in expression: ~A" expr)
(/ (calc-eval (second expr)) divisor))))
(t (error "Unknown operator: ~A" (first expr)))))
;; 3 + 4 * 2 => (+ 3 (* 4 2))
(calc-eval '(+ 3 (* 4 2))) ;; => 11Parsing an Infix String into an S-Expression
If the goal is to accept ordinary infix input like the string "3 + 4 * 2" rather than a pre-built S-expression, the calculator needs a small tokenizer and a precedence-aware parser before evaluation can happen: tokenizing splits the string into symbols like 3, +, 4, *, 2, and a recursive-descent parser then groups higher-precedence operators like * and / more tightly than + and -, producing the nested list structure (+ 3 (* 4 2)) shown earlier. This two-stage design—parse to a tree, then evaluate the tree—is the same architecture used by full-scale interpreters and compilers, just at a much smaller scale.
Cricket analogy: Tokenizing and parsing infix input is like a scorer converting a commentator's spoken description into structured scorecard entries before any statistics can be calculated from it.
You don't need a full tokenizer/parser to get started: Common Lisp's own reader already parses S-expression syntax for you, so (read-from-string "(+ 3 (* 4 2))") gives you a ready-to-evaluate list directly, which is why many beginner LISP calculators skip infix parsing entirely and just accept S-expression input.
Handling Errors and Edge Cases
A production-quality calculator needs to handle division by zero, unknown operators, and malformed input gracefully rather than crashing with an unhelpful stack trace; Common Lisp's error function signals a condition that can be caught with handler-case, letting the calling code decide whether to report a friendly message, retry with different input, or propagate the failure. Testing edge cases explicitly—(calc-eval '(/ 5 0)), (calc-eval '(% 5 2)) with an undefined operator, or deeply nested expressions—during development catches bugs that simple happy-path testing would miss.
Cricket analogy: Handling division by zero gracefully is like a scoring app catching the edge case of a team being all out for zero without bowling a single over, displaying 'N/A' instead of crashing on an undefined average calculation.
Common Lisp's / function returns an exact rational number for integer division, so (/ 7 2) evaluates to 7/2, not 3.5 or 3. If your calculator is meant to behave like a typical decimal calculator, explicitly coerce results with (float (/ 7 2)) or use / with at least one floating-point argument to get 3.5.
- Arithmetic expressions map naturally onto LISP S-expressions since operators are just prefix function calls, e.g.
(+ 3 (* 4 2)). - A recursive evaluator dispatches on the operator symbol at the head of each sub-list and recursively evaluates operands.
- Parsing infix strings into S-expressions requires a tokenizer and a precedence-aware recursive-descent parser, but
read-from-stringcan bypass this if you accept S-expression input directly. - Use
errorandhandler-caseto gracefully handle division by zero, unknown operators, and malformed input. - Common Lisp's
/returns exact rationals for integer arguments; coerce to float explicitly if decimal output is expected. - Testing edge cases like zero divisors and unknown operators during development catches bugs missed by happy-path testing alone.
Practice what you learned
1. How is the infix expression `3 + 4 * 2` naturally represented as a LISP S-expression respecting precedence?
2. In the recursive calc-eval function, what does the base case handle?
3. What Common Lisp function can parse an S-expression directly from a string like "(+ 3 (* 4 2))"?
4. What does `(/ 7 2)` evaluate to in Common Lisp?
5. What mechanism should the calculator use to gracefully handle division by zero instead of crashing?
Was this page helpful?
You May Also Like
LISP vs Modern Languages
A comparison of LISP's core design choices—homoiconicity, macros, and minimal syntax—against mainstream languages like Python, JavaScript, and Java.
LISP Best Practices
Practical conventions for writing clean, maintainable LISP code, covering naming, recursion style, macro hygiene, and error handling.
LISP Quick Reference
A condensed cheat sheet of core LISP syntax, data structures, and special forms for Common Lisp, covering the operations used daily.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics