Clojure Tutorial
Summary: Learn Clojure syntax, set up a development environment, then build a fun project.
Introduction
Want to learn Clojure? Well, this is the place for you!
Objectives
- Master the syntax of Clojure.
- Install everything you need to develop in Clojure.
- Write code using the REPL.
- Build a fun app in Clojure.
Learning Clojure can be challenging. It has different syntax (lots of parentheses), a different development model (REPL-Driven Development), and is mainly functional, which may be a new paradigm to you.
However, people who make it through say all of the work is worth it. Learning Clojure has changed how I program for the better, regardless of the language I'm using.
I've tried to make this as fun, smooth, and complete as possible. So let's get started!
Table of Contents
- Introduction
- Quick intro: What is Clojure?
- Fizzbuzz: An existing project
- Phrase-o-tron: An existing project
- Setting up a Clojure Dev environment
- 99 Bottles of Beer
- Setting up a Clojure Dev environment
- Rock, Paper, Scissors: A complete project
- Create a Clojure project
- Create a Clojure source file
- Jack-in to the REPL
- Print our first message
- Creating a main function
- Rich comment form
- Prompting for input and reading
- Branching
- Looping
- Playing the game
- Interpreting the input
- Playing a round
- Printing the result
- Maintaining state
- Printing the state of the game
- Review
- Next steps
Quick intro: What is Clojure?
Clojure is a functional programming language. It runs on the Java Virtual Machine (JVM). It has a different syntax from what you may be used to, but the syntax is simple. Most people pick it up quickly.
People use Clojure for any number of applications, from web development (backend and frontend), to machine learning, to financial services. It shines in multi-threaded programming, data processing, and exploratory programming, among many other strengths. It's used at some of the largest companies in the world and across all industries.
Fizzbuzz: An existing project
Fizzbuzz is a simple program often used to weed out people from interviews who can't manage a loop and a conditional. For us, it will be a great demonstration of how to loop and branch in Clojure.
In Fizzbuzz, we need to print out the numbers from 1 to 100, but if it's divisible by 3, we print Fizz, if it's divisible by 5, we print Buzz, and if it's divisible by both 3 and 5, we print FizzBuzz.
Here's some code. There's one problem in it, but we'll fix that in a minute. But first, let's understand it line-by-line.
(ns fizz-buzz.core)
(defn -main [& args]
(dotimes [n 100]
(cond
(and (zero? (rem n 3))
(zero? (rem n 5)))
(println "FizzBuzz")
(zero? (rem n 3))
(println "Fizz")
(zero? (rem n 5))
(println "Buzz")
:else
(println n))))
Let's go through each line.
(ns fizz-buzz.core)
This line defines the namespace our code will live in. A namespace is a unit of organization within Clojure programs that helps you manage your code and avoid name conflicts. For such a simple program, it's probably not worth having a namespace. But you will often see it, so I wanted to include it.
ns
defines a namespaceNamespaces have two or more segments, separated by .
In this case, we have the
segments fizz-buzz
and core
. core
is a common namespace final segment.
Here are some questions and answer choices. Choose the best answer for each one.
- What is the purpose of a namespace in Clojure?
- How does Clojure separate segments in a namespace?
- How do you define a namespace in Clojure?
The next line defines a function:
(defn -main [& args]
This is a Clojure convention. -main
is the name of the function that will be
called when running a namespace from the command line. The function will be
passed the command-line arguments entered in the shell. The arguments are
defined by the part in square brackets [& args]
. The &
is a special symbol
indicating that the arguments will be collected into a list.
defn
defines a function
(dotimes [n 100]
dotimes
executes a loop that iterates a fixed number of times. It's one of
the several ways to loop in Clojure. In this case, we're executing 100 times.
Each time through, the local variable n
will be bound to the current number.
The first time through, n
will be 0, the second time 1, then all the way up
to the last time, where n
will be 99.
dotimes
iterates a fixed number of times
(cond
cond
branches. It stands for conditional. Each branch needs a test and an
expression, which is what to do if the test is true. This cond
has four
branches. The cond
goes through each branch one at a time, checking the
test. The first one to return true
will execute.
cond
branches to the first branch with a true test
Here's the first branch:
(and (zero? (rem n 3))
(zero? (rem n 5)))
(println "FizzBuzz")
The test is the first expression, starting with and
. It's checking if the
number is divisible by 3 and divisible by 5. Let's dive into the expression.
Sometimes it's good to read Clojure expressions from the inside out.
and
does a logical AND operation
(rem n 3)
rem
is the remainder function. It divides the first argument by the second
argument (here n
and 3
) and returns the remainder.
rem
returns the remainder of a divisionrem
Below is a code editor. Edit the code to satisfy the prompts at the bottom of this box.
Edit the code to see the result.
- Find the remainder of 5/3
??
- Find the remainder of 6/2
??
- Find the remainder of 123/4
??
(zero? (rem n 3))
We then pass that return value (the remainder) to zero?
which returns true
if its argument is equal to zero. If the remainder is zero, it means the number
is divisible by 3.
We see the same for 5:
(zero? (rem n 5))
Then we combine them with and
:
(and (zero? (rem n 3))
(zero? (rem n 5)))
and
does a logical AND operation. It returns true if both of its arguments are true.
and
Below is a code editor. Edit the code to satisfy the prompts at the bottom of this box.
Edit the code to see the result.
- What is
true
ANDfalse
???
- What does
and
return when called with no arguments???
- Call
and
withtrue
,true
, andfalse
as arguments.??
Finally, the expression gets executed if the test is true:
(println "FizzBuzz")
This calls the println
function with the argument "FizzBuzz"
. println
prints its argument to the terminal, followed by a newline.
println
prints a string to standard out
The second branch prints "Fizz" if n
is divisible by 3
.
(zero? (rem n 3))
(println "Fizz")
The third branch prints "Buzz" if n
is divisible by 5
.
(zero? (rem n 5))
(println "Buzz")
The final branch is different.
:else
(println n)
Its test is :else
, a keyword. Keywords in Clojure start with a colon (:
).
They typically represent names of things. Here, though, it's being used for
another purpose. Clojure's cond
takes branches, each being a pair of test
and an expression. It doesn't have a default expression. So we're using
:else
as a test that always passes. Why does it always pass? In Clojure,
most values are considered "truthy" for the purpose of conditional tests. The
only two that are not are false
and nil
. Everything else will always make
the test pass. Using a keyword :else
is a very common way of making a
default branch in a cond
.
keywords are a Clojure data type that represent names. Their literal
representation starts with a colon (:
)
The expressions of this branch simply prints the number as is.
If we run it, though, we get this output:
FizzBuzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
...
The first thing printed is FizzBuzz, followed by 1. What's happening? The loop starts at 0 instead of 1. Likewise, the end of the output is 99 when it should be 100.
How can we fix that? Unfortunately, this is the behavior of dotimes
. dotimes
is still a great tool for this, but we have to add 1 to n
. We can add in a
local variable to store n+1
.
(ns fizz-buzz.core)
(defn -main [& args]
(dotimes [n 100]
(let [n (inc n)] ;; bind a local n to n+1
(cond
(and (zero? (rem n 3))
(zero? (rem n 5)))
(println "FizzBuzz")
(zero? (rem n 3))
(println "Fizz")
(zero? (rem n 5))
(println "Buzz")
:else
(println n))))
We use let
to define local variables. The variables are defined in the
binding form, which is the part inside the square brackets ([]
) that
immediately follows the symbol let
.
let
binds local variables
(dotimes [n 100]
(let [n (inc n)]
(cond
In this case, we're binding one variable, named n
, to the value of (inc n)
. inc
stands for increment, and it's Clojure's built-in function for
adding one to a number. Adding one is so common, it's worth having its own
function for it.
inc
increments a number by one
Some people will wonder what is going on since we have two variables called n
,
one bound by the dotimes
and one by the let
. That's a good point and I
should explain.
First of all, there are two different variables with the same name. It's not the
same variable being assigned to twice. Each variable has a scope (which means
the area of the code over which the variable is defined). In this case, the n
from dotimes
has a slightly bigger scope since the let
is inside the
dotimes
.
When you reuse a variable's name, it is called shadowing. Shadowing is a
common practice in Clojure. It lets you reuse a name and indicate that
shadowed variable should not be used within a scope. Inside of the body of the
let
, we can't refer to n
of the dotimes
. This can prevent errors. We
only want to refer to the n
of the let
which has the correct value.
Shadowing is reusing a variable name
And that's it! That's a Clojure implementation of FizzBuzz.
Phrase-o-tron: An existing project
Speaking of buzz, here's a neat little program that can generate new buzzword-compatible business ideas.
(ns phrase-o-tron.core)
(def adjectives ["web-scale" "streaming" "distributed" "mobile-first" "turn-key" "climate-friendly"])
(def services ["sneezing" "laundry" "napping" "chewing" "socializing"])
(def descriptors ["on the blockchain" "using AI" "in the cloud" "for the metaverse" "as a service"])
(defn -main [& args]
(let [adj (rand-nth adjectives)
srv (rand-nth services)
des (rand-nth descriptors)]
(println adj srv des)))
It outputs lines like this:
climate-friendly chewing for the metaverse
web-scale laundry on the blockchain
Let's go through it line-by-line.
(ns phrase-o-tron.core)
First, we define a namespace called phrase-o-tron.core
. Everything defined
after this line will be defined in the namespace.
(def adjectives ["web-scale" "streaming" "distributed" "mobile-first" "turn-key" "climate-friendly"])
This defines a var in the namespace. Vars (not variable) are named constants we can refer to in the namespace. They're called vars for historical reasons. They can do many things, but one that's most relevant to us now is that they can be re-defined during development with a new value. It's the same var but with a different value. That's important for REPL-Driven Development, which we'll see when we write our own code soon. However, they should be thought of as constants that don't change while the program runs.
def
defines a var in the namespace with a name and an initial value
This var is called adjectives
. Its value is a vector of strings. Vectors are
Clojure's built-in sequence type that can be indexed quickly by integers. They
are similar to arrays but they are immutable. We create a vector using the
literal square bracket syntax ([]
). Notice that we don't need commas (,
).
A vector is a sequence of values that can be indexed by integers. The
literal syntax is square brackets ([]
).
(def services ["sneezing" "laundry" "napping" "chewing" "socializing"])
(def descriptors ["on the blockchain" "using AI" "in the cloud" "for the metaverse" "as a service"])
We define two more vars, services
and descriptors
, with two more vectors of
strings.
(defn -main [& args] ;; args will be a sequence of command-line arguments
Again, the -main
function is the entry point into a Clojure program. This will
be passed the command-line arguments.
(let [adj (rand-nth adjectives)
srv (rand-nth services)
des (rand-nth descriptors)]
We define three locals in this let
, called adj
, src
, and des
. We
assign each one a random element from the three vectors we defined above. Each
one is assigned a value by calling rand-nth
on one of the vectors we define
above. rand-nth
selects a random element from a sequence. nth
is a
function that takes an integer n
and a sequence, and returns the nth element
from the sequence. rand-nth
doesn't take the integer n
, it generates an
n
randomly. Now we have three random strings that we can put together.
rand-nth
chooses and returns a random element from a collection
(println adj src des)
We print the three strings out, with spaces between them. It should output one line each time it runs. The lines should look something like this:
climate-friendly chewing for the metaverse
web-scale laundry on the blockchain
web-scale socializing in the cloud
99 Bottles of Beer
Here's a neat program that prints the lyrics for the song 99 Bottles of Beer. You have all the tools you need to understand this code. Step through each line to see what it does.
(ns bottles-99.core)
(defn -main [& args]
(dotimes [iteration 99]
(let [iteration (- 99 iteration)
next-iteration (- iteration 1)
word (if (> iteration 1) "bottles" "bottle")
word2 (if (> next-iteration 1) "bottles" "bottle")]
(println iteration word "of beer on the wall,")
(println iteration word "of beer.")
(println "Take one down.")
(println "Pass it around.")
(if (> next-iteration 0)
(println next-iteration word2 "of beer on the wall.")
(println "No more bottles of beer on the wall."))
(println))))
Setting up a Clojure Dev environment
We're going to build an app from scratch, but first, we need to get our development environment set up. We're going to set up the basic tools you need plus an IDE. In theory, you can use whatever IDE you feel comfortable with, but I've chosen VS Code with Calva because it is very popular and has an easy setup. All of the screenshots and keystrokes will use Calva.
For this tutorial, you will need four things:
- Java Development Kit (JDK) which includes the JVM and libraries.
- Clojure command line interface (CLI) which runs Clojure.
- Visual Studio Code (VS Code), an open-source code editor.
- Calva, a plugin for VS Code that supports Clojure development.
I have a guide for installing Clojure that covers the three major platforms (Windows, MacOS, and Linux). That guide goes way more in depth and detail. If you have trouble with the installation in this tutorial, check out that guide.
Install Java Development Kit
Download and install the latest OpenJDK LTS (long-term service) release from Adoptium.
Install the Clojure CLI
This step depends on what kind of system you are working on. Choose your system and follow the instructions.
Windows
Open a PowerShell terminal.
- Install Scoop (click here).
- Install dependencies
scoop install git ## if you don't have it already
scoop bucket add extras
- Add the Clojure repository
scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure
- Install Clojure CLI
scoop install clj-deps
MacOS
Open a Terminal window.
- Install Brew (click here) if you don't have it.
- Install Clojure
brew install clojure/tools/clojure
Linux
- Install dependencies.
sudo apt-get install -y bash curl rlwrap
- Download the install script.
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh
- Add execute permissions to the install script.
chmod +x linux-install.sh
- Execute the install script.
sudo ./linux-install.sh
Install Visual Studio Code
If you already have VS Code, you can skip this step.
First, get the latest version of VS Code for your system on the VS Code download page.
On Windows, it is an executable installer. Run it.
On Mac, it is a zip file containing the executable. Uncrompress the zip file then drag the application to your Applications folder.
On Linux, install the appropriate package as is typically done on your system. For instance, for Ubuntu, download the DEB file and double-click it.
Install Calva
Calva is a plugin for VS Code for editing and running Clojure code.
To install, first open VS Code. On the left there will be various icons. Click the Plugins icon.
In the search box, type "Calva". The Calva plugin should be near the top of the list. Its icon looks like this:
Click the little blue "install" button and follow the directions.
Update Calva settings
Calva comes with a great setup by default. However, there is one setting that will be difficult for beginners that we will want to turn off—Paredit.
Paredit is a code-editing mode for doing what is called structured editing. Structured editing is a style of code editing where parentheses are always balanced. That means you can't just add and remove parentheses at any time. Instead, there are commands for expanding and collapsing balanced sets of parentheses and other operations that always maintain open and close parens. I use Paredit, but I don't think it's a good idea to learn those commands at the same time as you are learning an editor and a programming language. So let's turn them off.
Go back to the plugins panel in VS Code, search for Calva, and click the little gear icon next to it. Click that and in the menu that pops up, click "Extension Settings".
That will open up the settings page. Along the left, there's a section called "Paredit". Click that. There are two settings you need to change.
- Set "Default Key Map" to "none".
- Uncheck "Hijack VSCode Defaults".
After setting those, you can close the settings tab.
Rock, Paper, Scissors: A complete project
Our first project is going to be a simple game: Rock, Paper, Scissors. In this game, the player will play against the computer. The computer will choose a random move, and the player will also choose a move. Then the game will print out the result and keep score.
We'll take it slow and build up the skills we need to write this game, including learning the syntax, managing input and output, and writing logic.
Let's get started.
Create a Clojure project
Let's create a new Clojure project in VS Code.
First, open the File > Open Folder...
option in the menu.
That will open a modal box. Navigate to the folder you want to put the new project in. The new project will be in a subfolder.
I keep my coding projects in a folder called projects
. You can put it wherever
you want.
Once inside your project folder, create a new folder by clicking the button at
the bottom of the modal. Name it rock-paper-scissors
.
Then click the Open button at the bottom right.
Now VS Code has an empty folder open. To make it a Clojure project, we need one
file called deps.edn
. Create a new file using the new file icon. Then name it
deps.edn
.
Once you have a deps.edn
file, you'll notice that Calva detects it and starts.
There's just one more thing we need to do.
In the deps.edn
file, which is now empty, add an opening and closing curly
brace ({}
). This is an empty Edn map. Edn is a data format based on
Clojure's syntax. We use it in the Clojure world. This one is empty now.
Eventually, this is where you'll put libraries you depend on and some other
project configuration.
Edn is a data format based on Clojure's syntax. The deps.edn
file
contains dependencies and other configuration for a Clojure project
Save deps.edn
. Now you've got a working Clojure project and Clojure IDE.
Create a Clojure source file
But our project doesn't do anything! Let's make a Clojure code file that we can fill with code.
We need to create an src
directory to hold our source files. Click the New Folder icon.
Call it src
.
Inside that folder, create a new folder called rock_paper_scissors
. Be sure to
use underscores. I'll talk about why shortly.
Finally, inside the new rock_paper_scissors
folder, create a new file called
core.clj
.
VS Code will open that file. Calva adds a single line up at the top. This line
is called the namespace declaration (or ns
declaration for short).
(ns rock-paper-scissors.game)
(If it doesn't create the namespace declaration, it could be that the Clojure LSP failed to load. Close the folder and open it again. And add the above line of code yourself.)
It defines a new Clojure namespace. Namespaces typically correspond to Clojure source files. They let you organize your code.
This namespace is called rock-paper-scissors.game
. Clojure namespaces need at
least two segments, separated by periods (.
). It's a typical pattern to see
the first segment name the project, and the main namespace being called game
.
As your project gets bigger, you add new namespaces as siblings to game
. For
instance, a namespace rock-paper-scissors.util
would be in a file util.clj
in the same folder as game.clj
.
This namespace corresponds to the file src/rock_paper_scissors/game.clj
.
Underscores are converted to hyphens (because hyphens are not universally
allowed in folder names), and slashes are converted to periods. Because the
namespace corresponds to a file on disk, the editor (VS Code with Calva) knew
how to generate the ns
declaration. There's more to it, but that will give you
an overview for now.
Jack-in to the REPL
Clojure programmers tend to use the REPL. The REPL stands for "Read Eval Print Loop". When we use the REPL, our IDE is connected directly to a live, running program, and we frequently update the running program (as our code changes) and run little snippets of code to test the system.
Calva comes with a command to start an instance of Clojure and connect to a REPL
in it. Open up the VS Code command palette by hitting Command-Shift P
(or
Control-Shift P
on Windows). Type "calva repl" to filter the list of commands.
Select "Calva: Start a Project REPL and Connect" from the list.
It will ask you what type of project to use. Click deps.edn
.
This will open the REPL in a window on the right side of the editor.
This is where the output of any code we evaluate will go. And we can also run code directly at the prompt.
Just for fun, let's test the REPL. After the prompt (clj:user:>
) type the following:
(+ 100 100)
Close the paren (or move the cursor to after the closing paren) and hit enter.
It should print the answer (200
) and then present a new prompt.
clj:user:=> (+ 100 100)
200
clj:user:=>
That's where the name "Read Eval Print Loop" comes from. The REPL read your code, evaluated it, printed the answer, then looped around to be ready for more input. Clojure programmers use the REPL a lot, but they often don't use the prompt directly. The editor has commands for running code from source files.
REPL stands for "Read Eval Print Loop". It reads the code, evaluates it, prints the result, and loops again.
Print our first message
Let's make our code do something. Type this into game.cj
.
(println "Welcome to the Rock, Paper, Scissors championship!")
Then load the whole file by hitting Alt-Control-c Enter
. That is, hold alt
and control, hit c, then let go and hit enter.
Alt-Control-c Enter
loads all the code in the file
Loading the file passes it through the Clojure compiler, then runs it. If we look in the REPL window, we see three things.
- The welcome string was printed out.
nil
was also printed.- The prompt changed from
clj:user>
toclj:rock-paper-scissors.game:>
.
clj:user:=>
Welcome to the Rock, Paper, Scissors championship!
nil
clj:rock-paper-scissors.game:=>
println
is the function to print out a string to standard out and then it
prints a newline. We called that function, passing it a string as a single
argument. Parens mean "function call" in Clojure.
Parentheses (()
) mean function call
The nil
that was printed is the return value of the last line of our file.
The last line of our file was the println
call. println
always returns
nil
.
The REPL will contain output for standard out and the printed return values.
The return value of println
is nil
Finally, the prompt changed because loading the file loaded the namespace. Calva is smart enough to move the REPL to the namespace of the current file.
Creating a main function
This is a good first step, but we don't want the message to load every time we
load the file. We only want it to run when we intend to run the program. Clojure
makes a distinction between loading the file and running the -main
function of
the file.
Let's define a -main
function. The -main
function is what is called when you
run a program.
Add this code to your game.clj
:
(defn -main [& args]
)
This is an empty function. Let's load the file. (Alt-Control-c Enter
).
Notice the string was printed out again in the REPL. But this time, it didn't
print nil
. Instead, the REPL prints #'rock-paper-scissors.game/-main
. We
don't need to go into that too much, just remember that loading the file prints
out the result of the last line of code. The last line of code defines the
function -main
.
We can call our function now, but it doesn't do anything! Let's make it do something.
Move the println
call into the -main
function.
Here's what your file should look like:
(ns rock-paper-scissors.game)
(defn -main [& args]
(println "Welcome to the Rock, Paper, Scissors championship!"))
Now load the file (Alt-Control-c Enter
).
Now it doesn't print the string when the file is loaded!
Rich comment form
We could call the -main
function at the REPL (try it if you like!). But
let's do something a little more idiomatic. Let's make a rich comment form.
We use the term form to mean a Clojure expression.
Form means a Clojure expression
At the end of the file, type the following:
(comment
(-main)
)
This is called a rich comment form. It's one of three ways to comment out
code in Clojure. It won't be run when you load the file. But it still has to
parse. The advantage is that you can run stuff inside of a rich comment form
with your editor with a keystroke. Let's do that now.
comment
creates a rich comment form
Move your typing cursor somewhere in the (-main)
function call. Then hit
Alt-Enter
. Alt-Enter
runs the current top-level form. The top-level
refers to two possible things:
Alt-Enter
runs the current top-level form
- The forms in your file that are not nested.
- The forms inside of a rich comment form that are not nested further.
When you run Alt-Enter
inside of a rich comment form, Calva will execute the
form where your cursor is. In this case, it ran -main
, which printed the
welcome message!
We now have a convenient way to run our game.
Prompting for input and reading
Add the following to the end of the -main
function:
(println "Ready to play? Type y, n, or q to quit.")
If you run -main
now, it won't print this new message, even after you save the
file. Try it. Move your cursor to the (-main)
inside the rich comment form
and hit Alt-Enter
.
Calva does not keep your code up-to-date in the REPL. You have to do it manually. It is a bit inconvenient, but I've learned to appreciate the control it gives me. It's definitely worth learning the keystrokes for loading files and executing top-level forms.
Let's compile the function. You can either load the whole file (Alt-Control-c Enter
) or move your cursor to the definition of -main
and evaluate the
top-level form (Alt-Enter
).
Run -main
to see it work. I suggest you run things frequently, perhaps more
frequently than you think you need, to figure out what's comfortable for you.
Programmers new to Clojure are often surprised by how often Clojure
programmers run their code. We run code even before we've finished writing it,
just to make sure the intermediate steps are correct.
Run your code frequently to check your work
Right now, the program just prints and then ends. Let's make it read input from the user.
Add the following line to the end of the -main
function:
(read-line)
Then recompile and re-run it.
You should see the same message printed in the REPL. But something else happened that may be hard to see. VS Code has this sneaky little input box that pops up at the top center of the screen. I didn't see it at first. Someone needed to point it out to me. It's black on black. So I'm pointing it out to you to save you some time and frustration. Don't worry, when we're running it at the command line (not in VS Code), it will print and read in the same terminal window.
Find the box at the top and type in "hello" then hit enter. You should see the same string printed to the REPL. Notice that it has quotes.
Why does it print to the REPL? Remember, the P in REPL stands for "print" it prints out the value of the expression you executed.
You executed (-main)
, which calls the function called -main
, which is defined like this:
(defn -main [& args]
(println "Welcome to the Rock, Paper, Scissors championship!")
(println "Ready to play? Type y, n, or q to quit.")
(read-line)) ;; the last line of the function
When they are called, functions return the last value of the last expression
in the body of the function. In this case, that is the call to read-line
.
read-line
returns the string it reads from that little box.
Functions return the value of their last expression
We don't want to return the value read from the box. We want to do something with it. Let's save it in a local variable.
Change the call to read-line
to look like this:
(let [line (read-line)]
line)
Now compile and run.
You should get the same behavior as before. However, now we introduce a local
variable called line
. This is called a let
form, which lets you bind names
to values. In this case, we're calling read-line
and binding the return value
to the name list
. We can now refer to the local variable list
anywhere after
it is defined within the let
, up until the closing paren that matches the
opening paren of the let
expression.
;; v-- this paren opens the let
(let [line (read-line)] ;; <-- the closing square brace starts the body
line) ;; <-- this paren closes the let
Branching
Let's do something with the local variable.
We want to branch on the three options we want to present. Let's take a small
step, creating a single if
expression:
(let [line (read-line)]
(if (= "y" line)
(println "Let's start the game!")
(println "I don't understand your input (yet)")))
Compile and run. Type "y" in the box, then hit enter. You should see our "Let's start the game!" message printed out.
Run it again and enter "n" in the box. It should print the "I don't understand your input (yet)" message.
Let's break down what is happening.
(let [line (read-line)] ;; read input and save to `line`
(if (= "y" line) ;; compare `line` to "y" with = (pronounced equal)
(println "Let's start the game!") ;; if they're equal, print this
(println "I don't understand your input (yet)"))) ;; else, print this
if
defines a two-branch conditional. If the first expression is true, it
executes the second expression. If it's false, it executes the third. =
compares two values for equality. So (= "y" line)
compares the string "y" to
the variable line
. So this expression prints one of two messages, depending
on what you type in the box.
if
is a two-branch conditional. =
compares two or more values for
equality
That was a really small step. Let's see if we can do all the branches in one go.
I count four ("y"
, "n"
, "q"
, and unknown):
(let [line (read-line)]
(if (= "y" line)
(println "Let's start the game!")
(if (= "n" line)
(println "You're not ready? Then I'll wait.")
(if (= "q" line)
(println "See you next time!")
(println "I don't understand your input. Please try again.")))))
Compile it. Now, play with it. You should be able to get each of the four messages, depending on what you enter in the box. Congrats!
Here's the thing: Clojure programmers don't want to nest so deeply. When we see
nested if
s like this, we want to change it to something flatter. There are
several options. For this tutorial, we will choose cond
, but you might also
want to explore
case
(for when you want to compare equality to literals) and
condp
(for when the predicates are all the same; in this case they're all =
).
We're choosing cond
because it's general-purpose and more useful to learn. And
it does the job just fine.
Here's the nested if
written as a cond
:
(let [line (read-line)]
(cond
(= "y" line) ;; test
(println "Let's start the game!") ;; expression
(= "n" line) ;; test
(println "You're not ready? Then I'll wait.") ;; expression
(= "q" line) ;; test
(println "See you next time!") ;; expression
:else ;; test
(println "I don't understand your input."))) ;; expression
It's much flatter (a single level of indentation).
Let's talk about cond
a moment. A cond
is a conditional that can have
unlimited branches. It is a bunch of pairs. The first part of the pair is the
test. The second part of the pair is the expression, which runs if the test
is true. The first true test will execute its expression. Only one will
run. If none are true, then none are run and the cond
returns nil
.
In this case, the cond
is checking whether the variable line
is equal to
"y"
, "n"
, and "q"
in turn. The first one that is equal will have its
branch executed.
There is no default case in a cond
, but you can easily make a default case
by making the last condition be a literal value that always evaluates as
truthy. Every value is truthy except nil
and false
. By convention, Clojure
programmers tend to use the keyword :else
because it is human-readable.
Since it's the last one, if none of the ones above it are true, the :else
branch will run.
All values are truthy except nil
and false
My personal preference is to separate the pairs with empty lines so they are easy to distinguish.
Try out the code. You should be able to reach every branch by giving different inputs.
Our -main
function is getting kind of long, which I don't like, but it's also
unclear where we should break it apart. I'm going to leave it as it is until
it's doing something more substantial.
Looping
We need to loop. Every option we choose simply prints and exits. Let's analyze what each option should actually do:
"y"
should start playing the game"n"
should wait for 5 seconds then ask if they're ready again"q"
should exit- else we should ask again for more input
Let's hold off on building the game and handle the other three cases. Writing those cases will help build the skills we need to do the game right.
First, let's wrap the let
with a loop
.
(defn -main [& args]
(println "Welcome to the Rock, Paper, Scissors championship!")
(println "Ready to play? Type y, n, or q to quit.")
(loop []
(let [line (read-line)]
(cond
(= "y" line)
(println "Let's start the game!")
(= "n" line)
(println "You're not ready? Then I'll wait.")
(= "q" line)
(println "See you next time!")
:else
(println "I don't understand your input.")))))
loop
is like a let
, but it lets you loop back up to the top with new
values of the bindings.
loop
indicates a place to iterate back to
We know in the default case, where we don't recognize the input, we need to ask again and loop.
Here's what that looks like:
(defn -main [& args]
(println "Welcome to the Rock, Paper, Scissors championship!")
(println "Ready to play? Type y, n, or q to quit.")
(loop []
(let [line (read-line)]
(cond
(= "y" line)
(println "Let's start the game!")
(= "n" line)
(println "You're not ready? Then I'll wait.")
(= "q" line)
(println "See you next time!")
:else
(do
(println "I don't understand your input.")
(recur))))))
This code is not exactly as I would write it. We can clean it up later. I'm
showing it because it lets us work in small steps. Let's zoom in and see what's
happening. The :else
branch is different from before:
:else
(do ;; do multiple things in a row
(println "I don't understand your input.")
(recur)) ;; recur means loop back to the top with a new value
Before, we were only doing one thing, so we could just put that thing as the
expression of the branch. But now, we need to do two things. To do multiple
things in a row and keep it as a single expression, we can wrap the things in a
_do
form.
do
forms execute each expression in sequence. The value of the do
form is
the value of the last expression inside it.
do
executes multiple expressions in sequence
The last expression is a call to recur
. recur
is a special expression in
Clojure that goes with loop
. It jumps to the beginning of the loop
but
with new values for the bindings. In this case, we have no arguments to
recur
because we have no bindings in the loop
.
recur
jumps to the beginning of the loop
form that contains it
Compile and run this function and enter in some unrecognized input. It will open the text input box again.
We want it to prompt us again to tell us the options we have, so let's move that
println
into the loop
:
(defn -main [& args]
(println "Welcome to the Rock, Paper, Scissors championship!")
(loop []
(println "Ready to play? Type y, n, or q to quit.")
(let [line (read-line)]
(cond
(= "y" line)
(println "Let's start the game!")
(= "n" line)
(println "You're not ready? Then I'll wait.")
(= "q" line)
(println "See you next time!")
:else
(do
(println "I don't understand your input.")
(recur))))))
Compile and run -main
again and try an unknown input. It should now print the "Ready
to play?" message after the unknown input. It's looping!
Then try to quit ("q"). It should quit the loop
since it's not running
recur
in that branch.
The next one we'll face is the "n"
branch. We need to recur
but only after it
waits 5 seconds.
(= "n" line)
(do ;; multiple expressions
(println "You're not ready? Then I'll wait.")
(Thread/sleep 5000) ;; wait 5,000 ms
(recur)) ;; loop again
Thread/sleep
is a built-in Java static method that will sleep the current
thread for a given number of milliseconds.
Thread/sleep
is a Java static method. Thread
(before the slash /
) is
the class name. sleep
(after the slash /
) is the method name
Compile, run, and make it go through the "n"
branch. Does it wait 5 seconds?
It should!
Playing the game
Our final branch to write is playing the game. Our function is already long, so let's break out the game playing into a new function.
Define a new function above -main
in the file, like this:
(defn play-game []
)
It doesn't do anything, but it's a good idea to try to compile it to make sure
the syntax is good. Move the cursor inside it and hit Alt-Enter
.
Now add a call to that function in the "y"
branch:
(= "y" line)
(do
(println "Let's start the game!")
(play-game)
(recur))
We add a recur
so that it will loop back to the menu when they quit the game.
Playing a game of Rock, Paper, Scissors is just a loop of the same thing over and over: You and your opponent choose Rock, Paper, or Scissors. We compare them. We figure out the result, and we update the score. Then we play again!
Let's set up the main loop of the game:
(defn play-game []
(loop []
(println "Choose (r)ock, (p)aper, (s)cissors, or (q)uit.")
(let [line (read-line)]
(println "You chose" line)
(recur))))
Compile and run that function. You may want to add (play-game)
to the rich
comment form to make it easier to run:
(comment
(-main)
(play-game))
It will keep prompting and reading forever. VS Code lets you hit Escape
to
exit the loop.
Interpreting the input
We're reading a line from the player, but we're not doing anything with it
except print it out. Let's make a branch and handle the "q"
case:
(let [line (read-line)]
(if (= line "q")
(println "Quitting game.")
(do
(println "You chose" line)
(recur))))
This quits the loop because in the "q"
branch it's not calling recur
. The
function should return and the loop in the -main
function should start again.
We're printing the letter that the player enters, but we want to do something with it. Right now, it's just a string. Clojure programmers tend to use keywords to represent named values. Here's how we're going to represent the three possible moves:
:rock
:paper
:scissors
We need to convert the string input from the user to one of those keywords. Clojure programmers tend to use a map to do this kind of conversion. It reads well as code and does the job nicely. Another option would be a function, but we've already seen functions and we need to learn maps. It's also more idiomatic to use a map in this case since the conversion is so simple.
Just above the definition of play-game
, define a top-level var like this:
(def ->move {"r" :rock
"p" :paper
"s" :scissors})
We're using def
to define a top-level var in the namespace. Var is the
correct name (not variable) in Clojure, but we won't get into why that is in
this tutorial. Vars are used to define constants. The name of this var is
->move
, which is pronounced: "to move". It's a convention that conversion
functions use ->
to indicate transforming from one thing to another.
The value of this var is a map. A map is a collection of key-value pairs. The
literal syntax uses curly braces ({}
). Maps let you look up a value by its
key in constant time. The keys and values can be of any types.
A map is a set of key-value pairs that can be indexed by key. The literal
syntax is curly braces ({}
)
We can then use the var as a lookup table to convert the input line. Let's do it
in the existing let
so we don't nest more than we need to:
(let [line (read-line)
move (get ->move line)] ;; convert user input to a keyword
(if (= line "q")
(println "Quitting game.")
(do
(println "You chose" move) ;; show the keyword chosen
(recur))))
We're using the get
function to look up the value in the map. The first
argument is the map to look up in. The second argument is the key.
get
looks up a value in a map given the key. get
returns nil
if it
can't find the key
I want to be able to see the result, so I've changed the println
to show the
move.
When possible, increase the amount of feedback you get from your code
Let's compile it and run it to see what happens. Try the following inputs:
"r"
, "p"
, "s"
, "q"
, and "j"
.
All of them work as expected, except "j"
which prints "You chose nil"
. "j"
is an invalid move, so we will want to have a branch to account for that. Let's
change the if
to a cond
and check for nil
.
(cond
(= line "q")
(println "Quitting game.")
(nil? move) ;; a nil move means it wasn't found in the map
(do
(println line "is an invalid move.")
(recur))
:else
(do
(println "You chose" move)
(recur)))
Try it again with these changes.
Playing a round
Well, we're all set to play a real round! Let's make a new function called
play-round
that takes the move the player chooses. Define it above play-game
and below ->move
.
(defn play-round [player-move]
)
Compile it. Then add a call to play-round
in the correct branch:
:else
(do
(println "You chose" move)
(play-round move)
(recur))
Let's think about what happens when we play a round. The player has already
chosen a move. The computer will have to choose a move. The computer will play
randomly. Then we compare the two moves. The result could be a win for the
player, a loss for the player, or a draw. The play-round
function can do all
of that and return the result.
Add a let
to store the computer's choice. For now, let's hardcode the choice
as :rock
so we can test it more easily:
(defn play-round [player-move]
(let [computer-move :rock]))
Now we want to compare the moves. The result will be represented as :win
,
:lose
, or :draw
. The easy one is :draw
. It happens when the moves are
equal.
(defn play-round [player-move]
(let [computer-move :rock]
(cond
(= player-move computer-move)
:draw)))
Compile and run the function and test if you can cause it to return :draw
. You
should be able to by calling (play-round :rock)
. Passing in :paper
and
:scissors
should return nil
.
The next step is a bit of a leap. You could use conditionals here to do it. If
you want, you can try it. However, I think there's a more elegant, data-driven
way. Just like with the ->move
lookup table, using data to represent
relationships is very common in Clojure.
Define a top-level var called beats
under the ->move
var like this:
(def beats #{[:rock :scissors]
[:scissors :paper]
[:paper :rock]})
This defines a top-level var called beats
. It's a set (defined with #{}
)
that will hold a "table" containing all the relationships of the form "x beats
y". Each "row" in the table is a vector of two items, the x and the y. Since
it has two items, a vector like this is often called a pair. I added spaces
to make it look nicer, like a table.
A set is a collection of unique values. You define them using a hash with
curly braces (#{}
)
We can use this set to determine if x beats y if we know x and y. Here's how:
(defn play-round [player-move]
(let [computer-move :rock]
(cond
(= player-move computer-move)
:draw
(contains? beats [player-move computer-move]) ;; check if the pair is in beats
:win
:else ;; not in beats, so it's a loss
:lose)))
The new branches decide if a round is a win or a loss for the player. We call
the function contains?
with two arguments, the beats
set and a pair of the
player's move and the computer's move. contains?
will check if the pair
exists in the set. If it does, it returns true
. If it does not, it returns
false
. If it's not in the set, it must be a loss.
contains?
checks if a value is in a collection
Compile it and run it with each of the three possible moves (:rock
, :paper
,
:scissors
).
Before you move on, make sure you understand what's happening. Using a data lookup where other languages might use conditional logic is very common in Clojure. It's a small win here, but for more significant situations it can be very powerful. It's one of the reasons people like Clojure.
We're now at the point where we can choose a random move for the computer. We will again drive the program from data. There are many ways to do this, but I'll take you through one way that I think will be educational.
First, let's define a top-level var with all of the moves. Put it between
->move
and beats
.
(def moves [:rock :paper :scissors])
That's again a vector of all the moves represented as keywords.
Compile it.
Now we can use a built-in function for choosing a random element from a sequence:
(let [computer-move (rand-nth moves)]
...)
Compile play-round
and try it out again. It's great, but now that I've done it
it seems like something is missing. How can we test it? We don't know what move
the computer has chosen.
Let's make the function play-round
return a map of the play in that round:
(defn play-round [player-move]
(let [computer-move :rock]
{:player player-move
:computer computer-move
:result (cond
(= player-move computer-move)
:draw
(contains? beats [player-move computer-move]) ;; check if the pair is in beats
:win
:else
:lose)))
Now when you run (play-round :paper)
you will see results like this:
{:player :paper, :computer :rock, :result :win}
{:player :paper, :computer :paper, :result :draw}
{:player :paper, :computer :scissors, :result :lose}
...
We can then manually evaluate whether they are correct.
That does it for play-round
.
Printing the result
We're calling play-round
in play-game
, but we're not doing anything with the
result. Let's start by printing out a message. Eventually, we will want to keep
the score.
For now, let's print a summary of the round. Replace the branch to play a round with this:
:else
(let [result (play-round move)
rstring (cond
(= :win (:result result))
"you win!"
(= :lose (:result result))
"you lose!"
(= :draw (:result result))
"it's a draw")]
(println "You:" (:player result) ", Computer:" (:computer result) "," rstring)
(recur))))
Let's step through this one line at a time. First, we're changing the do
to
let
. let
has the same idea of executing its body expressions in sequence. We
use a let
so we can bind names. We first bind result
to the return value of
play-round
. This will store the result of that round.
Next, we bind rstring
to text that describes the result. In this case, we're
using a cond
to branch on the result and choose an appropriate string. We're
also using a handy feature of keywords that let them be used as functions.
When you call a keyword like a function, it looks itself up in a map. In this
case, (:result result)
is equivalent to (get result :result)
.
Keywords can be used as functions. They look themselves up in a map
Then we print out the result. This will print a line like this:
You: :rock , Computer: :paper , you lose!
It's not perfect, but it's readable.
Finally, we recur
to play another round.
Let's make the string slightly better. There are two problems I'd like to fix:
- There are spaces before the commas, which is not correct punctuation.
- Keywords have
:
, which is valid Clojure but not what the player expects.
The first is due to the behavior of println
. It prints all of its arguments
with spaces between them. To have tight control over spaces, we need to pass it
a single argument with the exact string we want to print. Let's do that.
We will need to concatenate strings. In Clojure, to concatenate strings, we use
the function str
. Here is the old println
:
(println "You:" (:player result) ", Computer:" (:computer result) "," rstring)
And here is a version using str
:
(println (str "You: " (:player result) ", Computer: " (:computer result) ", " rstring))
We're building up a string, which will be the single argument to println
.
Note also that we had to add spaces where we wanted them because str
does
not.
str
concatenates strings
Now it prints like this:
You: :rock, Computer: :paper, you lose!
Problem 1 is solved. Now let's get rid of the extra colons. What's happening is
that str
, like println
, converts the keywords to strings in a way that
includes the initial colon. Sometimes that's what you want. But if you want to
convert a keyword to a string without the colon, you have to do a little bit
more work. And to do that, you'll need to understand the structure of keywords.
Keywords have two parts to them:
- Name
- Namespace
Many keywords don't have anything for a namespace. They are unqualified. That's what we have. Some examples of unqualified keywords:
:rock
:paper
:draw
:result
They're just a name, which is the string "rock", "paper", etc.
Some keywords have namespaces. They look like this:
:rock-paper-scissors.move/rock
:rock-paper-scissors.move/paper
:rock-paper-scissors.result/draw
:rock-paper-scissors.round/result
I made up namespaces for those keywords. Notice the namespace comes before the name. They are separated by a slash. These are qualified keywords, or sometimes namespaced keywords. Adding a namespace makes your keywords more universally useful. For instance, if you find a keyword in some piece of data somewhere on the internet, if it has a namespace, you can better understand what it's for. You can track down the code that is meant to read it. And you can be more sure that nobody else is using it, especially if you include the domain name of the project in the namespace. This may seem like overkill, but it's very useful as your community grows and it's common practice in languages like Java.
Okay, so how do we get the string representation of a keyword without the
colon? Well, in our our case, we can use the function name
, which returns
the name part of the keyword. There's also a function called namespace
that
returns the namespace portion (or nil
if it doesn't have one).
name
returns the name part of a keyword. namespace
returns the namespace
part
Wow! Let's add that now.
(println (str "You: " (name (:player result)) ", Computer: " (name (:computer result)) ", " rstring))
Now it prints this:
You: rock, Computer: paper, you lose!
Maintaining state
We're printing a nice message and playing in a loop. We can generate lots of wins, losses, and draws, but we're not remembering them. Let's keep track of the number of each one.
We want to maintain state for each iteration of the loop. To do that, we can add
a binding to the loop
expression, inside of the square brackets ([]
). Let's
add a binding called score
to the loop
in play-game
. It will be a map of
the three types of results, where the values start at zero and will be added to
each round.
(defn play-game []
(loop [score {:win 0 :lose 0 :draw 0}]
(println "Choose (r)ock, (p)aper, (s)cissors, or (q)uit.")
...))
When we put a binding in a loop
, the name will be initialized when we enter
the loop.
loop
takes bindings like let
Try to compile play-game
. You should get an error printed to the REPL.
; Syntax error (IllegalArgumentException) compiling recur at (src/rock_paper_scissors/game.clj:50:11).
; Mismatched argument count to recur, expected: 1 args, got: 0
Notice the second line of the error message. It's expecting one argument to
recur
but it got none. When you have bindings in a loop
, the corresponding
recur
s need the same number of arguments. Since we have one binding, we need
one argument to recur
. The value of that argument will be the value of the
binding in the next iteration of the loop. By doing this, we can maintain
state that is different each time through the loop.
recur
needs an argument for each binding in the loop
Let's do the easy one first. Here's our code when we detect an invalid move:
(nil? move)
(do
(println line "is an invalid move.")
(recur))
This call to recur
needs an argument. We don't want to change the score since
it's an invalid move. So we pass in the score unchanged.
(nil? move)
(do
(println line "is an invalid move.")
(recur score))
If we try to compile again, we get the same error. We need to add the argument
to the other call to recur
.
Here's the branch where we play a round:
:else
(let [result (play-round move)
rstring (cond
(= :win (:result result))
"you win!"
(= :lose (:result result))
"you lose!"
(= :draw (:result result))
"it's a draw")]
(println (str "You: " (name (:player result)) ", Computer: " (name (:computer result)) ", " rstring))
(recur))
What do we want to pass to recur
? We want the current score, but with the new
result added to it. There is a widespread pattern in Clojure that will become
easy with time, but when learning it it's kind of difficult. So let's jump into
the REPL for a minute. That way, we can play with the code to understand it.
In the REPL, define a var with the same kind of map as we had for the score.
(def score {:win 0 :lose 0 :draw 0})
Now, let's imagine we just had a win. We want to add one to the wins in that map so that it looks like this:
{:win 1 :lose 0 :draw 0}
Adding is easy. Especially adding one. In Clojure, there's a function called
inc
that adds one to a number. But the number is inside of the map. We could
pull the number out, add one, then put the number back. Here's what that looks
like:
(let [w1 (:win score) ;; pull out the number
w2 (inc w1)] ;; change it
(assoc score :win w2)) ;; put it back in
Execute this in the REPL. You'll see that it works. This is fine. But in
Clojure, this is such a common pattern that there's a function that does the
same thing in one go. It's called update
. Here's what it looks like:
(update score :win inc)
It may seem a little cryptic at first. Run it and try it. Then I'll explain.
We pass three arguments to update
: the map, the key, and a function.
update
does what we just did with the let
expression above. It takes the
value for the key out of the map, calls the function on it, then puts the
result of the function back into the map under the same key.
update
changes a value in a map using a function
Like I said, this is so common that we get used to reading this shorter version.
And it's so short we can put it right into the argument position of recur
:
:else
(let [result (play-round move)
rstring (cond
(= :win (:result result))
"you win!"
(= :lose (:result result))
"you lose!"
(= :draw (:result result))
"it's a draw")]
(println (str "You: " (name (:player result)) ", Computer: " (name (:computer result)) ", " rstring))
(recur (update score :win inc)))
Compile this and run it. It should give you the wrong behavior. What's wrong?
We're always incrementing :win
regardless of the result.
We need to tell it what to increment. We notice that the score
map contains
the same keys as we return in the :result
of the round. I did that on purpose
because I kind of anticipated this. Because they're the same keys, we can use
the :result
of the round directly, like this:
(recur (update score (:result result) inc))
Add that code and compile it, then play a game. Well, it isn't very fun because
we're not printing it out. Let's print the map out directly to ensure things are
working right. At the top of the loop
, print out the score
:
(defn play-game []
(loop [score {:win 0 :lose 0 :draw 0}]
(println score)
(println "Choose (r)ock, (p)aper, (s)cissors, or (q)uit.")
...))
Then compile and run. It should show you the score incrementing.
Clojure feels so lovely because many things are easily printable. We can print all of the built-in values without any extra work. This means you can get much faster feedback about the behavior of your code.
Printing the state of the game
Although it was a useful step for us programmers, it wouldn't do to show it to the user. We want a better string. That should be easy since we've already seen the pattern. Try out this code:
(println (str "Wins: " (:win score) ", Losses: " (:lose score) ", Draws: " (:draw score)))
Compile everything (it's a good idea to compile the whole file from time
to time just in case you've forgotten to compile a function that has changed)
with Control-Alt-c Enter
. Then run -main
.
Well, what a fun game!! (not!)
But I hope you had fun writing it.
Review
In this tutorial, we reviewed some existing code and wrote a whole rock-paper-scissors game on the command line. We installed and configured a Clojure IDE in VS Code. We learned about the REPL and how to use it to evaluate code.
Next steps
Your Clojure journey is just getting started. This tutorial can be a first step. Here are the steps I suggest you take next:
-
Re-enable paredit and learn how to use it. It will take time, but that time will be well-invested. Clojure programmers think in and edit the structure of their code by moving parentheses around instead of typing them all. We use a lot of parens, and it's easy to miss one. That's why paredit helps us manage them.
- Calva's official Paredit Guide. It visually shows what each command does. It's a good way to understand what kinds of operations are available.
- The Calva Paredit Cheatsheet is a handy reference. It's more compact than the official guide, so it's great to have open in a tab for quick reference.
-
Learn about Clojure's data structures. Clojure has a rich set of data structures and functions to operate on them. It is impossible to master Clojure without mastering its data structures.
-
Do some Clojure exercises