Writing Your Own Lisp Interpreter in Haskell - Part 3
16 Feb 2025Introduction
In our previous post, we introduced persistent variables into our Lisp interpreter, making it possible to store and retrieve values across expressions.
Now, it’s time to make our Lisp smarter by adding conditionals and logic.
In this post, we’ll extend our interpreter with:
ifexpressions- Boolean logic (
and,or,xor,not) - String support for conditionals
- Expanded numeric comparisons (
<=,>=)
By the end of this post, you’ll be able to write real conditional logic in our Lisp, compare both numbers and strings, and use logical expressions effectively.
Adding if Statements
We start with the classic Lisp conditional expression:
(if (< 10 5) "yes" "no") ;; Expected result: "no"
(if (= 3 3) "equal" "not equal") ;; Expected result: "equal"We add support for this by adjusting eval:
eval env (List [Atom "if", condition, thenExpr, elseExpr]) = do
result <- eval env condition
case result of
Bool True -> return thenExpr -- Return without evaluating again
Bool False -> return elseExpr -- Return without evaluating again
_ -> throwError $ TypeMismatch "Expected boolean in if condition" resultTesting
We can see this in action now:
λ> (if (> 10 20) "yes" "no")
"no"
λ> (if (= 42 42) "match" "no-match")
"match"
λ> (if #f 10 20)
20Expanding Boolean Logic
Now, we’ll add some boolean operators that are standard in conditionals:
(and ...)→ Returns#tif all values are#t.(or ...)→ Returns#tif at least one value is#t.(xor ...)→ Returns#tif exactly one value is#t.(not x)→ Returns#tifxis#f, otherwise returns#f.
These functions get added to Eval.hs:
booleanAnd, booleanOr, booleanXor :: [LispVal] -> ThrowsError LispVal
booleanAnd args = return $ Bool (all isTruthy args) -- Returns true only if all args are true
booleanOr args = return $ Bool (any isTruthy args) -- Returns true if at least one arg is true
booleanXor args =
let countTrue = length (filter isTruthy args)
in return $ Bool (countTrue == 1) -- True if exactly one is true
notFunc :: [LispVal] -> ThrowsError LispVal
notFunc [Bool b] = return $ Bool (not b) -- Negates the boolean
notFunc [val] = throwError $ TypeMismatch "Expected boolean" val
notFunc args = throwError $ NumArgs 1 args
isTruthy :: LispVal -> Bool
isTruthy (Bool False) = False -- Only #f is false
isTruthy _ = True -- Everything else is trueThese functions get added as primitives to our Lisp:
primitives =
[ ("not", BuiltinFunc notFunc),
("and", BuiltinFunc booleanAnd),
("or", BuiltinFunc booleanOr),
("xor", BuiltinFunc booleanXor)
]Testing
We can now exercise these new built-ins:
λ> (and #t #t #t)
#t
λ> (or #f #f #t)
#t
λ> (xor #t #f)
#t
λ> (not #t)
#fWe now have a full suite of logical operators.
Strings
Before this point, our Lisp has been very number based. Strings haven’t really seen much attention as our focus has been on putting together basic functionality first. With conditionals being added into our system, it’s time to give strings a little bit of attention.
First job is to expand = to also support strings.
numericEquals [Number a, Number b] = return $ Bool (a == b)
numericEquals [String a, String b] = return $ Bool (a == b) -- Added string support
numericEquals args = throwError $ TypeMismatch "Expected numbers or strings" (List args)Testing
We can see this in action now with a string variable:
λ> (define name "Joe")
"Joe"
λ> (if (= name "Joe") "yes" "no")
"yes"
λ> (if (= name "Alice") "yes" "no")
"no"More Numeric Comparators
To round out all of our comparison operators, we throw in implementations for <= and >=.
numericLessThanEq [Number a, Number b] = return $ Bool (a <= b)
numericLessThanEq args = throwError $ TypeMismatch "Expected numbers" (List args)
numericGreaterThanEq [Number a, Number b] = return $ Bool (a >= b)
numericGreaterThanEq args = throwError $ TypeMismatch "Expected numbers" (List args)These also require registration in our primitive set:
primitives =
[ ("<=", BuiltinFunc numericLessThanEq),
(">=", BuiltinFunc numericGreaterThanEq)
]Conclusion
We’ve added some great features to support conditional process here. As always part 3 of the code to follow this tutorial is available.