Part 8 — A complete beginner’s guide to Computer Programming with Clojure: Functions

Harvey Ellams
15 min readJan 2, 2021
Photo by Roman Mager on Unsplash

A function is just a piece of code that performs a specific task and can be ‘called’ when required. You have seen functions used throughout this course. For instance, mathematical + and println are examples of inbuilt functions. Often, but not always, a function expects an argument or arguments.

For example,

(+ 5 10)

+ is the function for addition.

5 and 10 are the arguments.

This function will work with 1 or more parameters.

Recall from Part 6, a parameter is like a placeholder where the function expects to receive an argument.

Arity

An empty function, known as 0 (zero) arity because it doesn’t take any arguments:

(defn emptyfn [ ] (* 10 15))(emptyfn)150

A 1-arity function that has one parameter x that takes only one argument:

(defn dublz [x] (* x 2))(dublz 10)20

Two or more arity is known as a Multi-arity function. In other words, you specify how many parameters you have to take arguments: So, a 2-arity function has 2 parameters that take 2 arguments:

(defn dublz2 [ x y ] ( * 2 x y))(deblz2 2 3)12

Here, the function has actually done this: 2 multiplied by 2 to give 4, then multiplied 4 by 3 to give 12.

However, if we write the function like this:

(defn dublz3 [x y] ( * 2 x ) ( * 2 y ))(dublz3 2 3)6

This time, the function just displayed the result of the last calculation e.g. 3 multiplied by 2.

We can check this by reversing the numbers.

(dublz3 3 2)4

Therefore, if we wish to take two numbers and display the double of both together side-by-side, we will need to write our function like so:

(defn dublz4 [ x y ] (println ( * 2 x ) ( * 2 y )))(dublz4 2 3)4 6

Variadic Functions

A Variadic function is any function that can accept a number of arguments but you don’t know exactly how many, or it varies.

Have a look at the following:

(defn mult [x & more] ( * x (apply * more)))(mult 3 3 3 3)81(mult 3 3 3 3 4)`324

This function has two extra parts, & and apply. The word more is just a parameter like y. So the above function would be the same as:

(defn mult [x & y] ( * x (apply * y)))

In this function, the & implies any number of arguments may follow, and these will be a list. The keyword apply is placed before the function to instruct it to apply it. In this case, it applies multiplication against each argument collected together as a list.

Note * , is the symbol for multiplication but it is also a function similar to our function called mult.

Consider the following:

(mult 7 8 9)504(* 7 8 9)504

This brings us to another point, a function can call another function.

We will make another function called multy that will call our function mult instead of *.

(defn multy [x & more] ( mult x (apply mult more)))(multy 7 8 9)504

Earlier, I stated apply applies multiplication to a list.

Consider the following:

The multiplication will not apply to a list. The same error will occur for other mathematical operators.

But if we add in our keyword apply

(apply * ‘(1 2 3 4 5 6))720

This is important to understand as our functions will often work with data, often as a list or a vector like so:

(apply * [1 2 3 4 5 6])720

All apply does is change this

(apply * [1 2 3 4 5 6])

to this

(* 1 2 3 4 5 6)

It does the same with text

(apply str ‘(“Star” “ Trek” “ — the Wrath of Khan”))“Star Trek — the Wrath of Khan”

or (to a vector)

(apply str [“Star” “ Trek” “ — the Wrath of Khan”])“Star Trek — the Wrath of Khan”

In other words, apply changes the above to

(str “Star” “ Trek” “ — the Wrath of Khan”)“Star Trek — the Wrath of Khan”

There is another built-in function (key-word) that works differently to apply, but produces the same result (usually).

The key-word is reduce

(reduce * ‘( 1 2 3 4 5 6))

Instead of a straight forward translation like apply, reduce process the list like so:

1 * 2 = 2

2 * 3 = 6

6 * 4 = 24

24 * 5 = 120

120 * 6 = 720

This is the equivalent of

(* (* (* (* (* 1 2) 3) 4) 5) 6)

It also works the same way with strings

(reduce str ‘(“Star” “ Trek” “ — the Wrath of Khan”))

which is interpreted thus:

“Star” + “ Trek” = “Star Trek”

“Star Trek” + “ — the Wrath of Khan” = “Star Trek — the Wrath of Khan”

So which one should you use?

There is much online debate about this. It is claimed that reduce is processed faster, especially against very large lists or vectors. In addition, they don’t always produce the same result. For example:

(apply hash-map [:Title “The Caves of Steel” :Author “Isaac Asimov” :Year “1954”])

translates to this

(hash-map :Title “The Caves of Steel” :Author “Isaac Asimov” :Year “1954”)

to produce

{:Title “The Caves of Steel”, :Year “1954”, :Author “Isaac Asimov”}

Whereas

(reduce hash-map [:Title “The Caves of Steel” :Author “Isaac Asimov” :Year “1954”])

produces

{{{{{:Title “The Caves of Steel”} :Author} “Isaac Asimov”} :Year} “1954”}

So, always check to see whether reduce works the way you expect it to.

Function example

Let’s revisit our Library database. This time we will improve on our database. A hash-map works upon key values. We will operate our Library database like so:

{:TITLE {Value {:AUTHOR Value,

:YEAR Value,

: ISBN} Value,

: NOTES Value }

}

}

For Example:

{:TITLE {“The Hobbit” {:AUTHOR “Tolkien”,                       :YEAR 1932                       :ISBN “9780788789823”                       :NOTES “ Prequel to The Lord of the Rings” },         “Star Trek” {:AUTHOR “Roddenberry”},        “Foundation” {:AUTHOR “Asimov”,                      :YEAR 1951}         }}

Therefore, to insert anything into the Library database we will specify a Title and another known value e.g. Author. If, for example, the author is unknown a value can be created e.g. “Anonymous” or “Unknown”. This way each Title value is paired with an author-value, even if the author is unknown.

First, we will define some variables, otherwise known as Atoms, to hold our data. The Library database will be a variable defined as an empty hash-map:

( def Library ( atom { } ) )

Note, empty brackets { } is simply a way of declaring an empty hash-map.

For Title, Author, Isbn, Year and Notes we will declare them as empty variables

( def Title ( atom nil ) )( def Author ( atom nil ) )( def Isbn ( atom nil ) )( def Notes ( atom nil ) )( def Year ( atom nil ) )

Now we will create a function to take in two arguments. Remember, arguments are what you provide to parameters. Our first function will take the Book Title and the Author. Have a look at the following:

(defn InsertAuth [ x y ](reset! Title x)(reset! Author y))

Here, x takes in the Book Title and y takes the Authors name. The values held by the atoms Title and Author are changed by the reset! function to ensure our variable will hold the new values.

Now, let’s add another line into our function to push these new variable values into our Library database (hash-map). Consider the following:

(defn InsertAuth [ x y ](reset! Title x)(reset! Author y)(swap! Library assoc-in [:TITLE @Title :AUTHOR ] @Author))

This works fine. Nevertheless, we will separate the last part into a separate function which we will then call. Breaking functions up into smaller functions makes the code easier to read and prevents the creation of huge, difficult to understand, monolith functions. We will create a new function just to insert our data into our Library:

(defn AuthFn [ ](swap! Library assoc-in [:TITLE @Title :AUTHOR ] @Author))
(defn InsertAuth [ x y ]
(reset! Title x)(reset! Author y)(AuthFn))

Now, to complete the task, our InsertAuth function calls the AuthFn function.

Decision making

Functions can make decisions. Consider the following:

( if ( number? 5)( println “true” )( println “false” ) )true

This function uses a keyword if to ask a question by calling another keyword number? If the answer is true it will immediately go to the next section ( println “true” ) and the function will stop there i.e. it will not go to the next section. However, if the answer is not true, it will skip the first section and, instead, process the last section ( println “false” ).

The general format is as follows

( if ( Question Answer )

(if the Answer is True execute this line only)

(if the Answer is False execute this line only) )

Therefore:

( if ( number? “A”)( println “true” )( println “false” ) )false

Let’s write a function that will only accept a number for the Year a book was published.

(defn YearFn [ ](if (number? @Year )( swap! Library assoc-in [:TITLE @Title :YEAR ] @Year)(println “Please input a number without quotes.\n\n” (reset! Year nil) ) ) )(defn InsertYear [ x y ](reset! Title x)(reset! Year y)(YearFn))

For clarity, let’s break down the relevant sections:

1. (if (number? @Year ) Look at what is stored in the Year variable and see if it is a number

2. ( swap! Library assoc-in [:TITLE @Title :YEAR ] @Year) If it is a number, then add it to the Library database

3. (println “Please input a number without quotes.\n\n” (reset! Year nil) If no number is stored in the variable, then a message will be printed out and the Year variable will be reset to nil.

To summarise, If number 1. is true then 2. will be executed. However, if number 1. is false, then number 3. will be executed.

You may be wondering about the \n at the end of the println statement. This is an example of formatting. The \n is not printed but is an instruction to print a new line. Hence two \n\n will print two new lines.

The \ is called an escape character. It escapes the character n so that n becomes a command for new line. Escape characters are used frequently. For example:

\t tab

\f formfeed

\b backspace

\r return

The escape character can also be used to display special characters by their octal number e.g:

The ISBN number consists of numbers, hyphens (-), and upper or lower case X (x). We will introduce the Regular Expression. A Regular Expression is often referred to as regex. Regex can be described as a set of characters that describe a searchable pattern. Regular Expressions will be looked at again in Part 9. For now, consider the following regex:

#”^[0–9xX\-]+$”

The # specifies that a regex pattern will follow ^ specifies the start of the line. 0–9 is any number from 0 to 9. The letters x and X. The minus is negated \ to produce a literal (hyphen). The + means one or more instances and $ is the end of the line. So, reading from left to right, match a string that has any positive integer from 0 to 9; and/or an x; and/or X; and/or -. The following would match:

“89789789–7878–8989”

“56656768676”

“67548–7878–898X”

Whereas, “67548–7878–898Y” would fail.

With this in mind, let’s write a function to only allow correctly formatted ISBN numbers into the Library database.

(defn ISBNFn [ ](if (re-find #”^[0–9xX\-]+$” @Isbn )( swap! Library assoc-in [:TITLE @Title :ISBN ] @Isbn)(println “An ISBN number only has numbers and hyphens and, occasionally, the letter X .\n\n” (reset! Isbn nil) ) ) )(defn InsertISBN [ x y ](reset! Title x)(reset! Isbn y)(ISBNFn))

Our ISBN function (ISBNFn) uses the key-word re-find to find the matching regex pattern inside the variable @Isbn. If it matches our pre-defined pattern, it will continue to the next section and update the Library database. Else, it jumps to the last section and prints a warning, and resets the Isbn variable.

Next, let’s work on the Notes function. It would be useful to restrict the number of characters inside Notes — somebody has to read them!

Before we tackle the Notes function, consider the count key-word:

(count “12345”)5(count “12 45”)5

Clearly, count counts the number of characters (including spaces) in between the quotes and returns a number.

The following details a string of characters held in the Notes variable:

If we apply the count function to @Notes:

We see @Notes holds a string of characters totaling 115.

Consider the following:

(if (< (count @Notes) 140)(println “True”)(println “False”))

This evaluates as: if 115 is < (less than) 140 then True, else it is False.

Here is our Notes function. This function will only accept notes that contain less than 140 characters.

( defn NotesFn [ ]( if (< ( count @Notes) 140 )( swap! Library assoc-in [:TITLE @Title :NOTES ] @Notes )( println “try again! There is a 140 character limit\n\n” ( reset! Notes nil ))))( defn InsertNotes [ x y ]( reset! Title x )( reset! Notes y )( NotesFn ))

Our Year function currently only accepts a number. Unfortunately, it can accept any number. We can improve the function with a regular expression. This time, allow it to accept a string but the string must be a number in the year format.

Consider the following:

#”^[12][0–9]{3}$”

This regex translates to:

^[12] — The first character is either a 1 or a 2.

[0–9]{3}$ — The remaining characters can be any number but must be 3 digits in length.

Therefore, 1927 and 2020 are acceptable. Whereas, 3020 would not.

Here is the new Year function:

(defn YearFn [ ](if (re-find #”^[12][0–9]{3}$” @Year )( swap! Library assoc-in [:TITLE @Title :YEAR ] @Year)(println “Please input a four digit year.\n\n” (reset! Year nil) ) ) )(defn InsertYear [ x y ](reset! Title x)(reset! Year y)(YearFn))

Let’s recap with what we have done so far to improve our Library database. First, we created an empty database consisting of true variables in the form of Atoms. We now know that an Atom is an essential part of the Functional programming paradigm and allows for much safer code execution as all threads or subprocesses will be executed without issue.

Specifically, the Atoms created are: Library, Title, Author, ISBN, Notes, and Year.

Next, we created 4 functions for inserting data into our Library database. Here, we introduced decision making and controls to help ensure the data inserted was correct or in a specific format. All 4 functions are multi-arity functions, that also call another function. Our insert functions are: InsertAuth, InsertYear, InsertISBN, InsertNotes.

To finish off, I have created both Find and Delete functions. They are all 1-arity functions. At this juncture, these functions should not require any explanation.

Find Functions

(defn FindBook [ x ](if (nil? (get-in @Library [:TITLE x ]))(println “Book not Found”)(println “The title “x “ has the following data stored “(get-in @Library [:TITLE x ]) )))(defn FindAuthor [ x ](if (nil? (get-in @Library [:TITLE x :AUTHOR]))(println “Nothing Found”)(get-in @Library [:TITLE x :AUTHOR])))(defn FindYear [ x ](if (nil? (get-in @Library [:TITLE x :YEAR]))(println “Nothing Found”)(get-in @Library [:TITLE x :YEAR])))(defn FindISBN [ x ](if (nil? (get-in @Library [:TITLE x :ISBN]))(println “Nothing Found”)(get-in @Library [:TITLE x :ISBN])))(defn FindNotes [ x ](if (nil? (get-in @Library [:TITLE x :NOTES]))(println “Nothing Found”)(get-in @Library [:TITLE x :NOTES])))

Delete Functions

(defn DelAuth [ x ](if (nil? (get-in @Library [:TITLE x :AUTHOR]))(println “Nothing to Delete”)(swap! Library update-in [:TITLE x ] dissoc :AUTHOR)))(defn DelYear [ x ](if (nil? (get-in @Library [:TITLE x :YEAR]))(println “Nothing to Delete”)(swap! Library update-in [:TITLE x ] dissoc :YEAR)))(defn DelISBN [ x ](if (nil? (get-in @Library [:TITLE x :ISBN]))(println “Nothing to Delete”)(swap! Library update-in [:TITLE x ] dissoc :ISBN)))(defn DelNotes [ x ](if (nil? (get-in @Library [:TITLE x :NOTES]))(println “Nothing to Delete”)(swap! Library update-in [:TITLE x ] dissoc :NOTES)))(defn DelBook [ x ](if (nil? (get-in @Library [:TITLE x ]))(println “Nothing to Delete”)(swap! Library update-in [:TITLE ] dissoc x)))

Saving the database as a file

The problem with creating Code in a REPL is you will lose everything as soon as you exit the REPL. We will talk about files in a later post. For the moment, we will look to see how we can save our database as a file.

First, we will need to import some pre-written functionality into our REPL session. This will allow us to both save the database and import it back into a new REPL session. Type in the following.

(require ‘[clojure.java.io :as io])(require ‘[clojure.edn :as edn])

My current Library looks like so

{:TITLE {“The Hobbit” {:AUTHOR “Tolkein”, 
:YEAR “1932”,
:ISBN “1932454–5644354–3234-X”,
:NOTES “Lorem ipsum …”},
“The Day of the Triffids” {:AUTHOR “Wyndham”}
}
}

Now I will save the Library (into the same directory the REPL is running). I will call it Library.txt.

(spit “Library.txt” (pr-str @Library))

I will then exit out of the REPL and start a completely new REPL session.

user=> exitBye for now!$ lein repluser=>

First, set up the necessary functionality.

(require ‘[clojure.java.io :as io])(require ‘[clojure.edn :as edn])

You will need to recreate an empty Atom with the same Library name.

( def Library ( atom { } ) )

Now, just fill it with the data from the Library.txt file.

(reset! Library (edn/read-string (slurp “Library.txt”)))

Just to check, I will use pprint (pretty-print) to display an easy-to-read version.

user=> (pprint @Library){:TITLE{“The Hobbit”{:AUTHOR “Tolkein”,:YEAR “1932”,:ISBN “1932454–5644354–3234-X”,:NOTES “Lorem ipsum …”},“The Day of the Triffids” {:AUTHOR “Wyndham”}}}niluser=>

However, you will need to retype the functions!

SUMMARY

In this post, we extended our knowledge of Maps and Atoms and even touched upon Regular Expressions (REGEX) and files. Nevertheless, the primary purpose here was to show you how to create your own functions. In addition, it is important to recognize that other programming languages (and programmers) may refer to a function as a Method or a Subroutine.

There is a lot of jargon and concepts written about functions. For instance, when a function is given a name (like we did with def) it is considered to be first-class. If the function does not have a name then it is an anonymous function. As it has no name, an anonymous function cannot be ‘called’ so tends to be used inside another function. A function can also be considered overloaded if it can be applied to different situations. For example, a function could be called Area and could calculate either the area of a square, triangle, or circle. It is considered overloaded as it is a function of functions under one name. All you need to do is find a way of ‘informing ’ the function of where it is being applied e.g. square, triangle, or circle.

Previous

Part 9 — REGEX

--

--