A Gentle Introduction to Interactive Programming
WORK IN PROGRESS. All pages subject to change.
Source for this book available here.
This book is intended for learners new to the world of software development, with little to no experience in programming. In this book, we will specifically use the Clojure programming language.
Clojure's powerful read-evaluate-print-loop (REPL), combined with a solid integrated development environment (IDE) in the form of Visual Studio Code will provide learners a tight feedback loop for learning code. This online book also features embedded interactive code snippets.
1. Introduction
Hello, hello! So, you want to learn programming. Whether that's because you want to bring an idea to life, or find computing fascinating, or just think it'll be fun, everyone has their own reason for learning how to code. I'd like show you how to learn programming in a way that's both practical and enjoyable.
First off, what actually is programming? Put simply, it's a way of telling a computer what you want it to do - generally in a very explicit, clear-cut way.
Computers are really good at taking some input, running some manipulations or calculations on it, and giving back some output. Both the input and the output are data.
What might our data be for input?:
- A text document
- A digital photo
- An MP3
- A spreadsheet of numbers
What calculations would we perform on those?:
- Spellchecking on the text
- Applying a filter to the photo
- Boosting the bass of the MP3
- Calculating tax information on the spreadsheet
We're getting ahead of ourselves, though. When you really get down to it, some of the above gets pretty technical.
Let's start with some arithmetic. Note: you do not need to know advanced math to be a programmer!
Imagine a math problem like 2 + 2
or 3 * 4
.
Very simple on its own, but if you think about it, you can keep combining
more and more simple problems to form a more difficult one. Say...
2 + 8 * 3 / 6 * (10 - 7)
Suddenly you've got a more complex problem, but so long as you follow some rules (PEMDAS / BODMAS), you can break it back down into smaller parts to make it easier to solve:
2 + 8 * 3 / 6 * (10 - 7)
2 + 8 * 3 / 6 * 3
2 + 24 / 6 * 3
2 + 4 * 3
2 + 12
14
Computing works in the same way: every complex procedure is just a series of smaller procedures, that can even be broken down and re-combined in different ways.
This book uses the Clojure programming, which is known to be particularly good for breaking complex problems into smaller, easier ones.
2. Let the Code Begin
Let's start telling our computers what to do, shall we? We'll start with the absolute basics: adding two numbers together.
(+ 2 2)
Already a lot going on, but don't worry.
Firstly, we're already living up to the title of this book. In the block above, you'll see two panes: the top is editable code, and the bottom is the result. Here, we can interactively write some code, and the result will appear to us instantly!
(That's nothing compared to what we'll be getting up to later, though 😉)
Second, that look awfully weird, doesn't it? Why the parentheses, and why does the plus sign come before the numbers?
This is a peculiar feature of Clojure that might look a little odd at first glance!
Here's what's going on - Clojure is composed of what we call expressions.
Expressions are just anything that can be evaluated and in turn produce values.
An expression can be as simple as the number 42
or the word "apples"
,
but they can also get more complicated, like a list of items.
When an expression is a list of items, we group those items together inside
parentheses - like in our list above (+ 2 2)
. When our expression is a list of
items like this, we call the first element the function and everything after
it the arguments.
In this case, the plus sign is an addition function. The numbers that follow are the arguments, so we're adding those numbers. Instead of "two plus two", think "add two and two".
You can write any number of numbers to be fed into this addition function. Unlike with typical math, you don't need to write a '+' between each number. This is really handy when trying to add just a bunch of numbers:
(+ 1 2 3 4 5 6 7 8 9)
And subtraction, multiplication and division work the same way! In the following code blocks, try changing the numbers, and/or adding some more numbers.
(- 10 3)
(* 5 5)
(/ 50 5)
We can also combine the results of expressions easily:
(+ 10 (* 5 5))
Here we add 10 to the result of multiplying five by five. When we chain a number of expressions together, we often write each expression on its own line, and indent each line to match up with its "level". For instance:
(+ 1
(+ 2 2)
(+ 3 3)
(+ 4
(+ 5 5)))
See how 1
, (+ 2 2)
, (+ 3 3)
, and the beginning of (+ 4 ...
are on all the same "level"?
(+ 5 5)
is nested within another expression, so we indent one "level" further.
2.1. Functions
We've seen functions for addition, subtraction, multiplication, and division. We're far from limited to those, though! Not only does Clojure have hundreds of functions built-in, we can define our own functions to use!
To define a function, we use defn
.
The form for this looks like:
(defn function-name
[input]
(do-something-with input))
Remember that this is a list, where the first item is defn
.
In this case:
- the second item is the name we want to give to the function we're defining
- the third item specifies what inputs (in square brackets) we want this function to work with
- the fourth item is an expression for what we're going to do with our input
For example:
(defn add-ten
[x]
(+ 10 x))
(add-ten 5)
Let's take a step back and break down exactly what we just did:
- we are defining a function named
add-ten
- this function takes one input, which we call
x
- we add 10 to whatever the value of
x
is
In the following code block, try writing completing this function that multiplies a value by five:
(defn times-five
[]
())
(times-five 4)
2.2. Function Practice
We can check if two values are equal using =
.
These practice problems gradually get more difficult.
(= 4
(+ 2 2))
Modify the following code examples, replacing the underscores with values,
so the editor prints true
.
(= 10
(+ 3 _))
(= 30
(* 5 _))
(= _
(/ 36 9))
(= _
(* 10
(+ 3 4)))
(defn times-five
[x]
(* 5 x))
(= _
(+ (times-five 4)
5))
Hint: there's nothing stopping you from using a value multiple times.
(defn square
[x]
(* _ _))
(= 9
(square 3))
Here's a new concept: we can define a value outside our function definition. We can still use that within our function, though.
(def pi 3.14)
(defn circle-circumference
[radius]
(* 2 _ _))
(= 12.56
(circle-circumference 2))
3. All About Data
We've discussed building series of operations together to form complex ones. We can do the same thing for data.
Think about this particular piece of data: a single word.
What can be built starting with a single word? Well,
- Words form sentences
- Sentences form paragraphs
- Paragraphs form chapters
- Chapters form books
- Books form libraries
Starting from something very small, you can build pieces together to form something huge. In much the same way, all complex pieces of data are fundamentally built from much smaller pieces gradually combined together.
Let's say we want to start writing a grocery list. How do we do that?: one item at a time.
We'll say for the moment that an item on our grocery list is just the name of that item, to keep it simple. In most programming languages, pieces of text are written surrounded by double quotes:
"apples"
Ah, but a grocery list has multiple items, of course. If we were to just write multiple pieces of text...
"apples"
"bananas"
"milk"
... there's nothing actually tying those things together.
I suppose we could also write them as a single piece of text...
"apples, bananas, milk"
... but that'd quickly get a bit messy when we have 20+ items, and especially when we need to sort them or perform some other operations.
They need to be individual pieces within some larger whole - a slightly more complex piece of data. In Clojure, one way of doing this is to write the individual pieces within square brackets, like so:
["apples", "bananas", "milk", "eggs", "bread"]
Much better! This particular piece of data is one we can easily perform useful operations on, such as sorting the items alphabetically:
(sort ["apples", "bananas", "milk", "eggs", "bread"])
We can easily, say, grab the first or last item of the grocery list:
(first ["apples", "bananas", "milk", "eggs", "bread"])
(last ["apples", "bananas", "milk", "eggs", "bread"])
Here we've introduced the functions sort
, first
, and last
-
all handy functions for working with series of data.
Here's one more: if we want to add a new item to our grocery list,
we conjoin an item to it with the conj
function:
(conj ["apples", "bananas", "milk", "eggs", "bread"] "juice")
Try writing your own grocery lists:
(first [])
(last [])
(conj [] "bananas")
Let's go one step further. In most American grocery stores, apples and bananas with be in the produce section, milk and eggs in the dairy section, and bread in the bakery.
To say that bread is in the bakery, we might create a mapping of the name of an item to the area of the store it's found in. We might write that map something like:
{"bread" "bakery"}
Within those curly brackets is a pair of two pieces of text. What we're saying with this data is that first value is associated with the second value.
For a grocery list, we probably want to flip this the other way around. When we shop at the store, we'll go to a section, and pick up everything we need in that section at once, so we don't bounce all over the store.
In that case, for each section of the store, we want that section to be associated with a smaller series of relevant items. In Clojure, we would write that something like this:
{"produce" ["apples", "bananas"],
"dairy" ["milk", "eggs"],
"bakery" ["bread"]}
Now you can see that we've combined three different types of data - pieces of text, series of items, and association of items - into a very useful structure.
3.1 Variables
You saw in the final exercise of chapter two that, in addition to defining our own functions, we can define variables as well.
Variables are pieces of data that we give a name to so we can easily reference them from any number of places.
We define a variable with def
:
(def four 4)
four
In addition to using variables in our function definitions, we can also use them when we're creating other variables. Going back to the final exercise of chapter two, here's an example:
(def pi 3.14)
(def radius 2)
(def circumference (* 2 pi radius))
circumference
Take the last grocery list structure we made, for example. We could define each of our categories of items as their own variables, then include them within the overall grocery list variable.
Try adding to the items we want to buy, or maybe even add a section, if you're up to it!
(def produce-items ["apples" "bananas"])
(def dairy-items ["milk" "eggs"])
(def bakery-items ["bread"])
(def grocery-list
{"produce" produce-items,
"dairy" dairy-items,
"bakery" bakery-items})
grocery-list
Alternatively, given a complex piece of data,
we can grab smaller pieces of data out of it.
Let's say we're in the dairy section of the
store, so we want to find the list of items that
we want to grab in this section.
We can do this this by using the get
function
on our grocery list with the name of the current section:
(def grocery-list
{"produce" ["apples", "bananas"],
"dairy" ["milk", "eggs"],
"bakery" ["bread"]})
(def current-section "dairy")
(def items-to-get
(get grocery-list current-section))
items-to-get
Breaking that down: we have a piece of data which are names of grocery store
sections associated with items that we want to buy in that section.
We have given this data the name grocery-list
by defining it as a variable.
We define another variable called current-section
with the section
of the store we're currently in, "dairy"
.
Then we get
, from our grocery list, the items associated
with the current section we're in.
Try changing the value of current-section
to "produce"
or "bakery
".
And again, if you're up to it, try adding a new section of your own!
4. Conditions
I want dessert. If there's apple pie in the house, I'll eat some apple pie, and if not, I'll have some ice cream.
Let's express that in code!
(if have-apple-pie?
(eat apple-pie)
(eat ice-cream))
This is called conditional logic:
- if [condition]
- then [do something]
- else [do a different thing]
In Clojure, this is expressed with the following form:
(if condition?
;; then
(do-thing)
;; else
(do-other-thing))
A bunch of things to cover here!
Additional new concept: those lines starting with ;;
are comments.
Comments are lines within code that are not evaluated.
They're a way of explaining code right next to the code itself.
Here I added two comments to make it clearer that the first
expression after the condition?
is what happens if condition?
is true, else the second expression happens.
Side note: conditions like this in Clojure typically have a question mark at the end to make it clear they're either true or false.
Let's introduce one more function that'll make things easier
for this section. The println
function prints a value
to its own line of text.
Let's combine a number of concepts we've learned so far: we'll have two number variables, then check if those numbers are equal. If they are, we'll print a success message, and if not, a failure message.
(def x 2)
(def y 3)
(if (= x y)
(println "The values are equal :)")
(println "The values are not equal :("))
Another common example is changing behavior based on whether a number is above or below some threshold.
(def apples-eaten 2)
(defn eat-another-apple
[]
(println "Keeping the doctor away for another day!"))
(if (> apples-eaten 3)
(println "You've eaten an unreasonable number of apples")
(eat-another-apple))
Here we check if the number of apples eaten so far is more than three, in which case we give the user an indication that maybe they should cool it with apples for the day. Otherwise, go ahead and eat another (if they want).