In the previous post we added
conditionals to our basic Lisp interpreter. Now, it’s time to introduce list manipulation – one of Lisp’s most
fundamental features.
This update brings support for:
Pairs (cons), first element (car), and rest (cdr)
List predicates (null?)
Common list operations (append, length, reverse)
Proper parsing of quoted lists ('(...))
Pairs and Lists
In Lisp, lists are built from pairs (cons cells). Each pair contains a head (car) and a tail (cdr). We’ll add Pair
into our LispVal data type:
(car and cdr)[https://en.wikipedia.org/wiki/CAR_and_CDR] are list primitives that allow you to return the first or
second component of a pair.
The expression (car (cons x y)) evaluates to x, and (cdr (const x y)) evaluates to y.
car::[LispVal]->ThrowsErrorLispValcar[List(x:_)]=returnxcar[Pairx_]=returnxcar[List[]]=throwError$TypeMismatch"Cannot take car of empty list"(List[])car[arg]=throwError$TypeMismatch"Expected a pair or list"argcarargs=throwError$NumArgs1argscdr::[LispVal]->ThrowsErrorLispValcdr[List(_:xs)]=return$Listxscdr[Pair_y]=returnycdr[List[]]=throwError$TypeMismatch"Cannot take cdr of empty list"(List[])cdr[arg]=throwError$TypeMismatch"Expected a pair or list"argcdrargs=throwError$NumArgs1args
We can now uwe these functions to work with our lists and pairs:
Going a little bit further now, we can easily implement append, length, and reverse.
listAppend::[LispVal]->ThrowsErrorLispVallistAppend[Listxs,Listys]=return$List(xs++ys)listAppend[Listxs,y]=return$List(xs++[y])listAppend[x,Listys]=return$List([x]++ys)listAppendargs=throwError$TypeMismatch"Expected two lists or a list and an element"(Listargs)listLength::[LispVal]->ThrowsErrorLispVallistLength[Listxs]=return$Number(toInteger(lengthxs))listLength[arg]=throwError$TypeMismatch"Expected a list"arglistLengthargs=throwError$NumArgs1argslistReverse::[LispVal]->ThrowsErrorLispVallistReverse[Listxs]=return$List(reversexs)listReverse[arg]=throwError$TypeMismatch"Expected a list"arglistReverseargs=throwError$NumArgs1args
These functions allow us to perform some more interesting processing of our lists:
(append'(12)'(34));; (1 2 3 4)(append'(ab)'c);; (a b c)(append'a'(bc));; (a b c)(length'(12345));; 5(length'());; 0(reverse'(12345));; (5 4 3 2 1)(reverse'());; ()
Quoted Lists
Finally, Lisp allows shorthand notation for quoting lists. For example, '(1 2 3) is equivalent to (quote (1 2 3)).
Our Lisp interpreter is now becoming a little more sophisticated. List processing is so fundamental to how Lisp operates
that we needed to get this implemented as soon as possible. The code for this particular article is available up on GitHub
to pull down and take a look at.
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:
if expressions
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:
evalenv(List[Atom"if",condition,thenExpr,elseExpr])=doresult<-evalenvconditioncaseresultofBoolTrue->returnthenExpr-- Return without evaluating againBoolFalse->returnelseExpr-- Return without evaluating again_->throwError$TypeMismatch"Expected boolean in if condition"result
Now, we’ll add some boolean operators that are standard in conditionals:
(and ...) → Returns #t if all values are #t.
(or ...) → Returns #t if at least one value is #t.
(xor ...) → Returns #t if exactly one value is #t.
(not x) → Returns #t if x is #f, otherwise returns #f.
These functions get added to Eval.hs:
booleanAnd,booleanOr,booleanXor::[LispVal]->ThrowsErrorLispValbooleanAndargs=return$Bool(allisTruthyargs)-- Returns true only if all args are truebooleanOrargs=return$Bool(anyisTruthyargs)-- Returns true if at least one arg is truebooleanXorargs=letcountTrue=length(filterisTruthyargs)inreturn$Bool(countTrue==1)-- True if exactly one is truenotFunc::[LispVal]->ThrowsErrorLispValnotFunc[Boolb]=return$Bool(notb)-- Negates the booleannotFunc[val]=throwError$TypeMismatch"Expected boolean"valnotFuncargs=throwError$NumArgs1argsisTruthy::LispVal->BoolisTruthy(BoolFalse)=False-- Only #f is falseisTruthy_=True-- Everything else is true
These functions get added as primitives to our Lisp:
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[Numbera,Numberb]=return$Bool(a==b)numericEquals[Stringa,Stringb]=return$Bool(a==b)-- Added string supportnumericEqualsargs=throwError$TypeMismatch"Expected numbers or strings"(Listargs)
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 >=.
In our previous post we put a very
simple Lisp interpreter together that was capable of some very basic arithmetic.
In today’s update, we’ll introduce variable definitions into our Lisp interpreter, allowing users to define and
retrieve values persistently. This required a number of structural changes to support mutable environments, error
handling, and function lookups.
All the changes here will set us up to do much more sophisticated things in our system.
If you’re following along, you can find the implementation for this article here.
Mutable Environment
In order to store variables in our environment, we need to make it mutable. This way we can store new values in the
environment as we define them.
Env was originally a pure Map:
typeEnv=MapStringLispVal
This meant that variable bindings were immutable and couldn’t persist across expressions.
We changed Env to an IORef (Map String LispVal), making it mutable:
typeEnv=IORef(MapStringLispVal)
We added nullEnv to create an empty environment:
nullEnv::IOEnvnullEnv=newIORefMap.empty
Why?
This allows variables to persist across expressions.
Future changes (like set! for modifying variables) require mutability.
IORef enables safe concurrent updates in a controlled manner.
REPL update
Now we need to update our REPL at the top level to be able to use this mutable state.
Previously, our REPL was just using the primitiveEnv value.
main::IO()main=doputStrLn"Welcome to Mini Lisp (Haskell)"replprimitiveEnv
We now pass it in as a value. Note that the underlying types have changed.
main::IO()main=doenv<-primitiveEnv-- Create a new environmentputStrLn"Welcome to Mini Lisp (Haskell)"replenv
Why?
The REPL now uses a mutable environment (primitiveEnv).
This ensures variables persist across expressions instead of resetting each time.
Variable Definition
We introduced defineVar to allow defining variables:
Lisp has long been a favorite language for those interested in metaprogramming, functional programming, and
symbolic computation. Writing your own Lisp interpreter is one of the best ways to deepen your understanding of
programming languages. In this series, we’ll build a Lisp interpreter from scratch in Haskell, a language that lends
itself well to this task due to its strong type system and functional nature.
In this first post, we’ll cover:
Defining Lisp’s syntax and core data structures
Writing a simple parser for Lisp expressions
Implementing an evaluator for basic operations
By the end of this post, you’ll have a working Lisp interpreter that can evaluate basic expressions like (+ 1 2).
If you’re following along, you can find the implementation for this article here.
Setup
We’re using Haskell to implement our Lisp interpreter, so make sure you’re installed and
ready to go.
To get started, create yourself a new project. I use stack so creating my
new list (called hlisp):
stack new hlisp
We’ll need a few dependencies to begin with. I’m adding my entire Lisp system to my library, leaving my main exe to
simply be a REPL.
In Haskell, we can represent these using a data type:
moduleExprwhereimportData.Map(Map)importqualifiedData.MapasMapimportControl.Monad.Except-- Lisp expression representationdataLispVal=AtomString|NumberInteger|BoolBool|List[LispVal]|Lambda[String]LispValEnv-- user-defined function|BuiltinFunc([LispVal]->ThrowsErrorLispVal)-- built-in functionsinstanceShowLispValwhereshow(Atomname)=nameshow(Numbern)=shownshow(BoolTrue)="#t"show(BoolFalse)="#f"show(Listxs)="("++unwords(mapshowxs)++")"show(Lambdaparamsbody_)="(lambda ("++unwordsparams++") "++showbody++")"show(BuiltinFunc_)="<builtin function>"instanceEqLispValwhere(Atoma)==(Atomb)=a==b(Numbera)==(Numberb)=a==b(Boola)==(Boolb)=a==b(Lista)==(Listb)=a==b_==_=False-- Functions and different types are not comparable-- Environment for variable storagetypeEnv=MapStringLispVal-- Error handlingdataLispError=UnboundVarString|TypeMismatchStringLispVal|BadSpecialFormStringLispVal|NotAFunctionString|NumArgsInt[LispVal]|ParserErrorStringderiving(Show)typeThrowsError=EitherLispError
This defines the core structure of Lisp expressions and introduces a simple error-handling mechanism.
We define Show and Eq explicitly on LispVal because of the Lambda and BuiltinFunc not really having naturally
expressed analogs for these type classes. The compiler complains!
LispVal allows us to define:
Atom
Number
Bool
List
Lambda
BuiltinFunc
The Env type gives us an environment to operate in keeping track of our variables.
LispError defines some high level problems that can occur, and ThrowsError is partially applied type where you’re
either going to receive the value (to complete the application), or as the type suggests - you’ll get a LispError.
Parsing Lisp Code
To evaluate Lisp code, we first need to parse input strings into our LispVal data structures. We’ll use the Parsec
library to handle parsing.
moduleParserwhereimportText.ParsecimportText.Parsec.String(Parser)importExprimportControl.MonadimportNumeric-- Parse an atom (symbol)parseAtom::ParserLispValparseAtom=dofirst<-letter<|>oneOf"!$%&|*+-/:<=>?@^_~"rest<-many(letter<|>digit<|>oneOf"!$%&|*+-/:<=>?@^_~")return$Atom(first:rest)-- Parse a numberparseNumber::ParserLispValparseNumber=Number.read<$>many1digit-- Parse booleansparseBool::ParserLispValparseBool=(string"#t">>return(BoolTrue))<|>(string"#f">>return(BoolFalse))-- Parse listsparseList::ParserLispValparseList=List<$>between(char'(')(char')')(sepByparseExprspaces)-- General parser for any expressionparseExpr::ParserLispValparseExpr=parseAtom<|>parseNumber<|>parseBool<|>parseList-- Top-level function to run parserreadExpr::String->ThrowsErrorLispValreadExprinput=caseparseparseExpr"lisp"inputofLefterr->Left$ParserError(showerr)Rightval->Rightval
With these parsers defined, we can now evaluate expressions.
Simple Evaluation
Now, we can use these types to perform some evaluations. We do need to give our interpreter some functions that it can
execute.
moduleEvalwhereimportExprimportControl.Monad.ExceptimportqualifiedData.MapasMap-- Look up variable in environmentlookupVar::Env->String->ThrowsErrorLispVallookupVarenvvar=caseMap.lookupvarenvofJustval->RightvalNothing->Left$UnboundVarvar-- Apply a function (either built-in or user-defined)apply::LispVal->[LispVal]->ThrowsErrorLispValapply(BuiltinFuncf)args=fargsapply(Lambdaparamsbodyclosure)args=iflengthparams==lengthargstheneval(Map.union(Map.fromList(zipparamsargs))closure)bodyelseLeft$NumArgs(lengthparams)argsapplynotFunc_=Left$NotAFunction(shownotFunc)-- Evaluator functioneval::Env->LispVal->ThrowsErrorLispValevalenv(Atomvar)=lookupVarenvvareval_val@(Number_)=Rightvaleval_val@(Bool_)=Rightvalevalenv(List[Atom"quote",val])=Rightvalevalenv(List(Atomfunc:args))=dofunc'<-evalenv(Atomfunc)args'<-mapM(evalenv)argsapplyfunc'args'eval_badForm=Left$BadSpecialForm"Unrecognized form"badForm-- Sample built-in functionsprimitives::[(String,LispVal)]primitives=[("+",BuiltinFuncnumericAdd),("-",BuiltinFuncnumericSub),("*",BuiltinFuncnumericMul),("/",BuiltinFuncnumericDiv)]numericAdd,numericSub,numericMul,numericDiv::[LispVal]->ThrowsErrorLispValnumericAdd[Numbera,Numberb]=Right$Number(a+b)numericAddargs=Left$TypeMismatch"Expected numbers"(Listargs)numericSub[Numbera,Numberb]=Right$Number(a-b)numericSubargs=Left$TypeMismatch"Expected numbers"(Listargs)numericMul[Numbera,Numberb]=Right$Number(a*b)numericMulargs=Left$TypeMismatch"Expected numbers"(Listargs)numericDiv[Numbera,Numberb]=ifb==0thenLeft$TypeMismatch"Division by zero"(Numberb)elseRight$Number(a`div`b)numericDivargs=Left$TypeMismatch"Expected numbers"(Listargs)-- Initialize environmentprimitiveEnv::EnvprimitiveEnv=Map.fromListprimitives
Creating a REPL
We can now tie all of this together with a REPL.
moduleMainwhereimportEvalimportParserimportExprimportControl.MonadimportSystem.IO-- REPL looprepl::Env->IO()replenv=doputStr"λ> "hFlushstdoutinput<-getLineunless(input=="exit")$docasereadExprinput>>=evalenvofLefterr->printerrRightval->printvalreplenvmain::IO()main=doputStrLn"Welcome to Mini Lisp (Haskell)"replprimitiveEnv
Formatting drives on any operating system can be a handful of instructions specific to that operating environment. In today’s post we’ll walk through the process of formatting a USB drive to FAT32, explaining each command along the way.
Identifying the Device
Before making any changes, you need to determine which device corresponds to your USB drive. The best way to do this is:
dmesg | grep da
or, for a more detailed view:
geom disk list
On FreeBSD, USB mass storage devices are typically named /dev/daX (where X is a number). If you only have one USB drive plugged in, it is likely /dev/da0.
Device naming in FreeBSD is quite uniform:
USB Drives: /dev/daX
SATA/SAS/IDE Drives: /dev/adaX
NVMe Drives: /dev/nvmeX
RAID Volumes: /dev/mfidX, /dev/raidX
Partitioning the Drive
Now that we know the device name, we need to set up a partition table and create a FAT32 partition.
Destroying Existing Partitions
If the drive has existing partitions, remove them:
gpart destroy -F /dev/da0
This ensures a clean slate.
Creating a Partition Table
We create a Master Boot Record (MBR) partition table using:
gpart create -s mbr /dev/da0
-s mbr: Specifies an MBR (Master Boot Record) partition scheme.
Other options include gpt (GUID Partition Table), which is more modern but may not be supported by all systems.
Adding a FAT32 Partition
Now, we create a FAT32 partition:
gpart add -t fat32 /dev/da0
-t fat32: Specifies the FAT32 partition type.
Other valid types include freebsd-ufs (FreeBSD UFS), freebsd-swap (swap partition), freebsd-zfs (ZFS), and linux-data (Linux filesystem).
After running this command, the new partition should be created as /dev/da0s1.
Formatting the Partition as FAT32
To format the partition, we use newfs_msdos:
newfs_msdos -L DISKNAME -F 32 /dev/da0s1
-L DISKNAME: Assigns a label to the volume.
-F 32: Specifies FAT32.
/dev/da0s1: The newly created partition.
Why /dev/da0s1 instead of /dev/da0?
When using MBR, partitions are numbered starting from s1 (slice 1), meaning that the first partition on da0 becomes da0s1. Using /dev/da0 would format the entire disk, not just a partition.
Wrapping Up
At this point, your USB drive is formatted as FAT32 and ready to use. You can mount it manually if needed: