Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
The Clojure Workshop
The Clojure Workshop

The Clojure Workshop: Use functional programming to build data-centric applications with Clojure and ClojureScript

Arrow left icon
Profile Icon Joseph Fahey Profile Icon Konrad Szydlo Profile Icon Yehonathan Sharvit Profile Icon Scott McCaughie Profile Icon Thomas Haratyk +1 more Show less
Arrow right icon
€32.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.3 (13 Ratings)
Paperback Jan 2020 800 pages 1st Edition
eBook
€17.99 €26.99
Paperback
€32.99
Subscription
Free Trial
Renews at €18.99p/m
Arrow left icon
Profile Icon Joseph Fahey Profile Icon Konrad Szydlo Profile Icon Yehonathan Sharvit Profile Icon Scott McCaughie Profile Icon Thomas Haratyk +1 more Show less
Arrow right icon
€32.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.3 (13 Ratings)
Paperback Jan 2020 800 pages 1st Edition
eBook
€17.99 €26.99
Paperback
€32.99
Subscription
Free Trial
Renews at €18.99p/m
eBook
€17.99 €26.99
Paperback
€32.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

The Clojure Workshop

2. Data Types and Immutability

Overview

In this chapter, we start by discovering the concept of immutability and its relevance in modern programs. We then examine simple data types such as strings, numbers and booleans, highlighting subtle differences in different environments like Clojure and ClojureScript. After a first exercise, we move on to more elaborated data types with collections such as lists, vectors, maps and sets, learning along the way which to use in different situations. After touching on the collection and sequence abstractions, we learn new techniques for working with nested data structures, before finally moving on to the final activity: implementing our very own in-memory database.

By the end of this chapter, you will be able to work with the commonly used data types in Clojure.

Introduction

Computer hardware has evolved dramatically in the last few decades. On a typical computer, storage and memory capacity have both increased a millionfold compared to the early 1980s. Nonetheless, standard industry practices in software development and mainstream ways of programming are not that different. Programming languages such as C++, Java, Python, and Ruby still typically encourage you to change things in place, and to use variables and mutate the state of a program, that is, to do things as if we were programming on a computer with a minimal amount of memory. However, in our quest for efficiency, better languages, and better tools, we reach for higher-level languages. We want to get further away from machine code. We want to write less code and let the computers do the tedious work.

We don't want to think about the computer's memory anymore, such as where a piece of information is stored and whether it's safe and shareable, as much as we don't want to know about the order of the instructions in the CPU. It is a distraction to the problems we are trying to solve, which are already complicated enough. If you have ever tried to do some multithreading in the languages cited previously, you will know the pain of sharing data between threads. Although, leveraging multicore CPUs with multithreaded applications is an essential part of optimizing a modern program's performance.

In Clojure, we work almost exclusively with immutable data types. They are safe to share, easy to fabricate, and improve the readability of our source code. Clojure provides the necessary tools to write programs with the functional programming paradigm: first-class citizen functions, which we will discover in the next chapter, and avoiding mutating and sharing the state of an application with immutable data types.

Let's dust off the dictionary and look up the definition of immutable, "Immutable: that cannot be changed; that will never change." It doesn't mean that a piece of information cannot change over time, but we record those modifications as a series of new values. "Updating" an immutable data structure provides a new value derived from the original value. However, the original value remains unchanged – those data structures that preserve previous versions of themselves are called persistent data structures.

Intuitively, we may think that such a persistent data structure would negatively impact performance, but it's not as bad as it seems. They are optimized for performance, and techniques such as structural sharing bring the time complexity of all operations close to classic, mutable implementations.

In other terms, unless you are programming an application that requires extraordinarily high performance, such as a video game, the benefits of using immutable data structures far outweigh the small loss in performance.

Simple Data Types

A data type designates what kind of value a piece of data holds; it is a fundamental way of classifying data. Different types allow different kinds of operations: we can concatenate strings, multiply numbers, and perform logic algebra operations with Booleans. Because Clojure has a strong emphasis on practicality, we don't explicitly assign types to values in Clojure, but those values still have a type.

Clojure is a hosted language and has three notable, major implementations in Java, JavaScript, and .NET. Being a hosted language is a useful trait that allows Clojure programs to run in different environments and take advantage of the ecosystem of its host. Regarding data types, it means that each implementation has different underlying data types, but don't worry as those are just implementation details. As a Clojure programmer, it does not make much difference, and if you know how to do something in Clojure, you likely know how to do it in, say, ClojureScript.

In this topic, we will go through Clojure's simple data types. Here is the list of the data types looked at in this section. Please note that the following types are all immutable:

  • Strings
  • Numbers
  • Booleans
  • Keywords
  • Nil

Strings

Strings are sequences of characters representing text. We have been using and manipulating strings since the first exercise of Chapter 1, Hello REPL.

You can create a string by simply wrapping characters with double quotes ("):

user=> "I am a String"
"I am a String"
user=> "I am immutable"
"I am immutable"

String literals are only created with double quotes, and if you need to use double quotes in a string, you can escape them with the backslash character (\):

user=> (println "\"The measure of intelligence is the ability to change\" - Albert Einstein")
"The measure of intelligence is the ability to change" - Albert Einstein
nil

Strings are not able to be changed; they are immutable. Any function that claims to transform a string yields a new value:

user=> (def silly-string "I am Immutable. I am a silly String")
#'user/silly-string
user=> (clojure.string/replace silly-string "silly" "clever")
"I am Immutable. I am a clever String"
user=> silly-string
"I am Immutable. I am a silly String"

In the preceding example, calling clojure.string/replace on silly-string returned a new string with the word "silly" replaced with "clever." However, when evaluating silly-string again, we can see that the value has not changed. The function returned a different value and did not change the original string.

Although a string is usually a single unit of data representing text, Strings are also collections of characters. In the JVM implementation of Clojure, strings are of the java.lang.String Java type and they are implemented as collections of the java.lang.Character Java type, such as the following command, which returns a character:

user=> (first "a collection of characters")
\a
user=> (type *1)
java.lang.Character

first returns the first element of a collection. Here, the literal notation of a character is \a. The type function returns a string representation of the data type for a given value. Remember that we can use *1 to retrieve the last returned value in the REPL, so *1 evaluates to \a.

It is interesting to note that, in ClojureScript, strings are collections of one-character strings, because there is no character type in JavaScript. Here is a similar example in a ClojureScript REPL:

cljs.user=> (last "a collection of 1 character strings")
"s"
cljs.user=> (type *1)
#object[String]

As with the Clojure REPL, type returns a string representation of the data type. This time, in ClojureScript, the value returned by the last function (which returns the last character of a string) is of the #object[String] type, which means a JavaScript string.

You can find a few common functions for manipulating strings in the core namespace, such as str, which we used in Chapter 1, Hello REPL!, to concatenate (combine multiple strings together into one string):

user=> (str "That's the way you " "con" "ca" "te" "nate")
"That's the way you concatenate"
user=> (str *1 " - " silly-string)
"That's the way you concatenate - I am Immutable. I am a silly String"

Most functions for manipulating strings can be found in the clojure.string namespace. Here is a list of them using the REPL dir function:

user=> (dir clojure.string)
blank?
capitalize
ends-with?
escape
includes?
index-of
join
last-index-of
lower-case
re-quote-replacement
replace
replace-first
reverse
split
split-lines
starts-with?
trim
trim-newline
triml
trimr
upper-case

As a reminder, this is how you can use a function from a specific namespace:

user=> (clojure.string/includes? "potatoes" "toes")
true

We will not cover all the string functions, but feel free to try them out now. You can always look up the documentation of a string function from the preceding list with the doc function.

Numbers

Clojure has good support for numbers and you will most likely not have to worry about the underlying types, as Clojure will handle pretty much anything. However, it is important to note that there are a few differences between Clojure and ClojureScript in that regard.

In Clojure, by default, natural numbers are implemented as the java.lang.Long Java type unless the number is too big for Long. In that case, it is typed clojure.lang.BigInt:

user=> (type 1)
java.lang.Long
user=> (type 1000000000000000000)
java.lang.Long
user=> (type 10000000000000000000)
clojure.lang.BigInt

Notice, in the preceding example, that the number was too big to fit in the java.lang.Long Java type and, therefore, was implicitly typed clojure.lang.BigInt.

Exact ratios are represented by Clojure as "Ratio" types, which have a literal representation. 5/4 is not an exact ratio, so the output is the ratio itself:

user=> 5/4
5/4

The result of dividing 3 by 4 can be represented by the ratio 3/4:

user=> (/ 3 4)
3/4
user=> (type 3/4)
clojure.lang.Ratio

4/4 is equivalent to 1 and is evaluated as follows:

user=> 4/4
1

Decimal numbers are "double" precision floating-point numbers:

user=> 1.2
1.2

If we take our division of 3 by 4 again, but this time mix in a "Double" type, we will not get a ratio as a result:

user=> (/ 3 4.0)
0.75

This is because floating-point numbers are "contagious" in Clojure. Any operation involving floating-point numbers will result in a float or a double:

user=> (* 1.0 2)
2.0
user=> (type (* 1.0 2))
java.lang.Double

In ClojureScript, however, numbers are just "JavaScript numbers," which are all double-precision floating-point numbers. JavaScript does not define different types of numbers like Java and some other programming languages do (for example, long, integer, and short):

cljs.user=> 1
1
cljs.user=> 1.2
1.2
cljs.user=> (/ 3 4)
0.75
cljs.user=> 3/4
0.75
cljs.user=> (* 1.0 2)
2

Notice that, this time, any operation returns a floating-point number. The fact that there is no decimal separation for 1 or 2 is just a formatting convenience.

We can make sure that all those numbers are JavaScript numbers (double-precision, floating-point) by using the type function:

cljs.user=> (type 1)
#object[Number]
cljs.user=> (type 1.2)
#object[Number]
cljs.user=> (type 3/4)
#object[Number]

If you need to do more than simple arithmetic, you can use the Java or JavaScript math libraries, which are similar except for a few minor exceptions.

You will learn more about host platform interoperability in Chapter 9, Host Platform Interoperability with Java and JavaScript (how to interact with the host platform and its ecosystem), but the examples in the chapter will get you started with doing some more complicated math and with using the math library:

Reading a value from a constant can be done like this:

user=> Math/PI
3.141592653589793

And calling a function, like the usual Clojure functions, can be done like this:

user=> (Math/random)
0.25127992428738254
user=> (Math/sqrt 9)
3.0
user=> (Math/round 0.7)
1

Exercise 2.01: The Obfuscation Machine

You have been contacted by a secret government agency to develop an algorithm that encodes text into a secret string that only the owner of the algorithm can decode. Apparently, they don't trust other security mechanisms such as SSL and will only communicate sensitive information with their own proprietary technology.

You need to develop an encode function and a decode function. The encode function should replace letters with numbers that are not easily guessable. For that purpose, each letter will take the character's number value in the ASCII table, add another number to it (the number of words in the sentence to encode), and finally, compute the square value of that number. The decode function should allow the user to revert to the original string. Someone highly ranked in the agency came up with that algorithm so they trust it to be very secure.

In this exercise, we will put into practice some of the things we've learned about strings and numbers by building an obfuscation machine:

  1. Start your REPL and look up the documentation of the clojure.string/replace function:
    user=> (doc clojure.string/replace)
    -------------------------
    clojure.string/replace
    ([s match replacement])
      Replaces all instance of match with replacement in s.
       match/replacement can be:
       string / string
       char / char
       pattern / (string or function of match).
       See also replace-first.
       The replacement is literal (i.e. none of its characters are treated
       specially) for all cases above except pattern / string.
       For pattern / string, $1, $2, etc. in the replacement string are
       substituted with the string that matched the corresponding
       parenthesized group in the pattern.  If you wish your replacement
       string r to be used literally, use (re-quote-replacement r) as the
       replacement argument.  See also documentation for
       java.util.regex.Matcher's appendReplacement method.
       Example:
       (clojure.string/replace "Almost Pig Latin" #"\b(\w)(\w+)\b" "$2$1ay")
       -> "lmostAay igPay atinLay"

    Notice that the replace function can take a pattern and a function of the matching result as parameters. We don't know how to iterate over collections yet, but using the replace function with a pattern and a "replacement function" should do the job.

  2. Try and use the replace function with the #"\w" pattern (which means word character), replace it with the ! character, and observe the result:
    user=> (clojure.string/replace "Hello World" #"\w" "!")

    The output is as follows:

    "!!!!! !!!!!"
  3. Try and use the replace function with the same pattern, but this time passing an anonymous function that takes the matching letter as a parameter:
    user=> (clojure.string/replace "Hello World" #"\w" (fn [letter] (do (println letter) "!")))

    The output is as follows:

    H
    e
    l
    l
    o
    W
    o
    r
    l
    d
    "!!!!! !!!!!"

    Observe that the function was called for each letter, printing the match out to the console and finally returning the string with the matches replaced by the ! character. It looks like we should be able to write our encoding logic in that replacement function.

  4. Let's now see how we can convert a character to a number. We can use the int function, which coerces its parameter to an integer. It can be used like this:
    user=> (int \a)
    97
  5. It seems that the "replacement function" will take a string as a parameter, so let's convert our string to a character. Use the char-array function combined with first to convert our string to a character as follows:
    user=> (first (char-array "a"))
    \a
  6. Now, if we combine previous steps together and also compute the square value of the character's number, we should be approaching our obfuscation goal. Combine the code written previously to obtain a character code from a string and get its square value using the Math/pow function as follows:
    user=> (Math/pow (int (first (char-array "a"))) 2)
    9409.0
  7. Let's now convert this result to the string that will be returned from our replace function. First, let's remove the decimal part by coercing the result to an int, and put things together in an encode-letter function, as follows:
    user=>
    (defn encode-letter
      [s]
      (let [code (Math/pow (int (first (char-array s))) 2)]
        (str (int code))))
    #'user/encode-letter
    user=> (encode-letter "a")
    "9409"

    Great! It seems to work. Let's now test our function as part of the replace function.

  8. Create the encode function, which uses clojure.string/replace as well as our encode-letter function:
    user=>
    (defn encode
      [s]
      (clojure.string/replace s #"\w" encode-letter))
    #'user/encode
    user=> (encode "Hello World")
    "518410201116641166412321 756912321129961166410000"

    It seems to work but the resulting string will be hard to decode without being able to identify each letter individually.

    There is another thing that we did not take into account: the encode function should take an arbitrary number to add to the code before calculating the square value.

  9. First, add a separator as part of our encode-letter function, for example, the # character, so that we can identify each letter individually. Second, add an extra parameter to encode-letter, which needs to be added before calculating the square value:
    user=>
    (defn encode-letter
      [s x]
      (let [code (Math/pow (+ x (int (first (char-array s)))) 2)]
        (str "#" (int code))))
    #'user/encode-letter
  10. Now, test the encode function another time:
    user=> (encode "Hello World")
    Execution error (ArityException) at user/encode (REPL:3).
    Wrong number of args (1) passed to: user/encode-letter

    Our encode function is now failing because it is expecting an extra argument.

  11. Modify the encode function to calculate the number of words in the text to obfuscate, and pass it to the encode-letter function. You can use the clojure.string/split function with a whitespace, as follows, to count the number of words:
    user=>
    (defn encode
      [s]
      (let [number-of-words (count (clojure.string/split s #" "))]
        (clojure.string/replace s #"\w" (fn [s] (encode-letter s number-of-words)))))
    #'user/encode
  12. Try your newly created function with a few examples and make sure it obfuscates strings properly:
    user=> (encode "Super secret")
    "#7225#14161#12996#10609#13456 #13689#10609#10201#13456#10609#13924"
    user=> (encode "Super secret message")
    "#7396#14400#13225#10816#13689 #13924#10816#10404#13689#10816#14161 #12544#10816#13924#13924#10000#11236#10816"

    What a beautiful, unintelligible, obfuscated string – well done! Notice how the numbers for the same letters are different depending on the number of words in the phrase to encode. It seems to work according to the specification!

    We can now start working on the decode function, for which we will need to use the following functions:

    Math/sqrt to obtain the square root value of a number.

    char to retrieve a letter from a character code (a number).

    subs as in substring, to get a sub-portion of a string (and get rid of our # separator).

    Integer/parseInt to convert a string to an integer.

  13. Write the decode function using a combination of the preceding functions, to decode an obfuscated character:
    user=>
    (defn decode-letter
      [x y]
      (let [number (Integer/parseInt (subs x 1))
            letter (char (- (Math/sqrt number) y))]
      (str letter)))
    #'user/decode-letter
  14. Finally, write the decode function, which is similar to the encode function except that it should use decode-letter instead of encode-letter:
    user=>
    (defn decode [s]
      (let [number-of-words (count (clojure.string/split s #" "))]
        (clojure.string/replace s #"\#\d+" (fn [s] (decode-letter s number-of-words)))))
    #'user/decode
  15. Test your functions and make sure that they both work:
    user=> (encode "If you want to keep a secret, you must also hide it from yourself.")

    The output is as follows:

    "#7569#13456 #18225#15625#17161 #17689#12321#15376#16900 #16900#15625 #14641#13225#13225#15876 #12321 #16641#13225#12769#16384#13225#16900, #18225#15625#17161 #15129#17161#16641#16900 #12321#14884#16641#15625 #13924#14161#12996#13225 #14161#16900 #13456#16384#15625#15129 #18225#15625#17161#16384#16641#13225#14884#13456."
    user=> (decode *1)
    "If you want to keep a secret, you must also hide it from yourself."

In this exercise, we've put into practice working with numbers and strings by creating an encoding system. We can now move on to learning other data types, starting with Booleans.

Booleans

Booleans are implemented as Java's java.lang.Boolean in Clojure or JavaScript's "Boolean" in ClojureScript. Their value can either be true or false, and their literal notations are simply the lowercase true and false.

Symbols

Symbols are identifiers referring to something else. We have already been using symbols when creating bindings or calling functions. For example, when using def, the first argument is a symbol that will refer to a value, and when calling a function such as +, + is a symbol referring to the function implementing the addition. Consider the following examples:

user=> (def foo "bar")
#'user/foo
user=> foo
"bar"
user=> (defn add-2 [x] (+ x 2))
#'user/add-2
user=> add-2
#object[user$add_2 0x4e858e0a "user$add_2@4e858e0a"]

Here, we have created the user/foo symbol, which refers to the "bar" string, and the add-2 symbol, which refers to the function that adds 2 to its parameter. We have created those symbols in the user namespace, hence the notation with /: user/foo.

If we try to evaluate a symbol that has not been defined, we'll get an error:

user=> marmalade
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: marmalade in this context

In the REPL Basics topic of Chapter 1, Hello REPL!, we were able to use the following functions because they are bound to a specific symbol:

user=> str
#object[clojure.core$str 0x7bb6ab3a "clojure.core$str@7bb6ab3a"]
user=> +
#object[clojure.core$_PLUS_ 0x1c3146bc "clojure.core$_PLUS_@1c3146bc"]
user=> clojure.string/replace
#object[clojure.string$replace 0xf478a81 "clojure.string$replace@f478a81"]

Those gibberish-like values are string representations of the functions, because we are asking for the values bound to the symbols rather than invoking the functions (wrapping them with parentheses).

Keywords

You can think of a keyword as some kind of a special constant string. Keywords are a nice addition to Clojure because they are lightweight and convenient to use and create. You just need to use the colon character, :, at the beginning of a word to create a keyword:

user=> :foo
:foo
user=> :another_keyword
:another_keyword

They don't refer to anything else like symbols do; as you can see in the preceding example, when evaluated, they just return themselves. Keywords are typically used as keys in a key-value associative map, as we will see in the next topic about collections.

In this section, we went through simple data types such as string, numbers, Boolean, symbols, and keywords. We highlighted how their underlying implementation depends on the host platform because Clojure is a hosted language. In the next section, we will see how those values can aggregate to collections.

Collections

Clojure is a functional programming language in which we focus on building the computations of our programs in terms of the evaluation of functions, rather than building custom data types and their associated behaviors. In the other dominant programming paradigm, object-oriented programming, programmers define the data types and the operations available on them. Objects are supposed to encapsulate data and communicate with each other by passing messages around. But there is an unfortunate tendency to create classes and new types of objects to customize the shape of the data, instead of using more generic data structures, which cascades into creating specific methods to access and modify the data. We have to come up with decent names, which is difficult, and then we pass instances of objects around in our programs. We create new classes all the time, but more code means more bugs. It is a recipe for disaster; it is an explosion of code, with code that is very specific and benefits from little reuse.

Of course, it is not like that everywhere, and you can write clean object-oriented code, with objects being the little black boxes of functionality they were designed for. However, as programmers, whether it's through using other libraries or maintaining a legacy code base, we spend most of our time working with other people's code.

In functional programming, and more specifically, in Clojure, we tend to work with just a few data types. Types that are generic and powerful, types that every other "Clojurian" already knows and has mastered.

Collections are data types that can contain more than one thing and describe how those items relate to each other. The four main data structures for collections that you should know about are Maps, Sets, Vectors, and Lists. There are more available, including the data structure offered by your host platform (for example, Java or JavaScript) or other libraries, but those four are your bread and butter for doing things in Clojure.

"Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming." - Rob Pike's Rule #5 of programming.

Maps

A Map is a collection of key-value pairs. Clojure provides – in a persistent and immutable fashion – the usual HashMap but also a SortedMap.

HashMaps are called "Hash" because they create a hash of the key and map it to a given value. Lookups, as well as other common operations (insert and delete), are fast.

HashMaps are used a lot in Clojure, notably, for representing entities where we need to associate some attributes to some values. SortedMaps are different because they preserve the order of the keys; otherwise, they have the same interface and are used in the same way as HashMaps. SortedMaps are not very common, so let's focus on HashMaps.

You can create a HashMap with the literal notation using curly braces. Here is a Map with three key-value pairs, with the keys being the :artist, :song, and :year keywords:

user=> {:artist "David Bowtie" :song "The Man Who Mapped the World" :year 1970}
{:artist "David Bowtie", :song "The Man Who Mapped the World", :year 1970}

You might have noticed in the preceding example that key-value pairs in the map are separated by a space, but Clojure evaluates it and returns a Map with key-value pairs separated by a comma. As with other collections, you can choose to use a space or a comma to separate each entry. For maps, there's no best practice and if you think it improves a map's readability, use commas; otherwise, simply omit them. You can also separate entries with new lines.

Here's another map written with comma-separated entries:

user=> {:artist "David Bowtie", :song "Comma Oddity", :year 1969}
{:artist "David Bowtie", :song "Comma Oddity", :year 1969}

Notice that the values can be of any type, and not only simple values such as strings and numbers, but also vectors and even other maps, allowing you to create nested data structures and structure information as follows:

user=>
  {
  "David Bowtie" {
    "The Man Who Mapped the World" {:year 1970, :duration "4:01"}
    "Comma Oddity" {:year 1969, :duration "5:19"}
  }
  "Crosby Stills Hash" {
    "Helplessly Mapping" {:year 1969, :duration "2:38"}
    "Almost Cut My Hair" {:year 1970, :duration "4:29", :featuring ["Neil Young", "Rich Hickey"]}
  }
}
{"David Bowtie" {"The Man Who Mapped the World" {:year 1970, :duration "4:01"}, "Comma Oddity" {:year 1969, :duration "5:19"}}, "Crosby Stills Hash" {"Helplessly Mapping" {:year 1969, :duration "2:38"}, "Almost Cut My Hair" {:year 1970, :duration "4:29", :featuring ["Neil Young" "Rich Hickey"]}}}

Keys can be of different types too, so you could have strings, numbers, or even other types as a key; however, we generally use keywords.

Another way of creating a map is by using the hash-map function, passing in pairs of arguments as follows:

user=> (hash-map :a 1 :b 2 :c 3)
{:c 3, :b 2, :a 1}

Choose to use literal notation with curly braces when possible, but when HashMaps are programmatically generated, the hash-map function can come in handy.

Map keys are unique:

user=> {:name "Lucy" :age 32 :name "Jon"}
Syntax error reading source at (REPL:6:35).
Duplicate key: :name

An exception was thrown because the :name key was present twice in the preceding literal map.

However, different keys can have the same value:

user=> {:name "Lucy" :age 32 :number-of-teeth 32}
{:name "Lucy", :age 32, :number-of-teeth 32}

Notice that both age and number-of-teeth have the same value, and that is both valid and convenient, to say the least.

Now that you know how to create maps, it is time for a bit of practice.

Exercise 2.02: Using Maps

In this exercise, we will learn how to access and modify simple maps:

  1. Start your REPL and create a map:
    user=> (def favorite-fruit {:name "Kiwi", :color "Green", :kcal_per_100g 61 :distinguish_mark "Hairy"})
    #'user/favorite-fruit
  2. You can read an entry from the map with the get function. Try to look up a key or two, as follows:
    user=> (get favorite-fruit :name)
    "Kiwi"
    user=> (get favorite-fruit :color)
    "Green"
  3. If the value for a given key cannot be found, get returns nil, but you can specify a fallback value with a third argument to get:
    user=> (get favorite-fruit :taste)
    nil
    user=> (get favorite-fruit :taste "Very good 8/10")
    "Very good 8/10"
    user=> (get favorite-fruit :kcal_per_100g 0)
    61
  4. Maps and keywords have the special ability to be used as functions. When positioned in the "operator position" (as the first item of the list), they are invoked as a function that can be used to look up a value in a map. Try it now by using the favorite-fruit map as a function:
    user=> (favorite-fruit :color)
    "Green"
  5. Try to use a keyword as a function to look up a value in a Map:
    user=> (:color favorite-fruit)
    "Green"

    As with the get function, those ways of retrieving a value return nil when the key cannot be found, and you can pass an extra argument to provide a fallback value.

  6. Provide a fallback value for a key that doesn't exist in the favorite-fruit map:
    user=> (:shape favorite-fruit "egg-like")
    "egg-like"
  7. We would like to store this value in the map. Use assoc to associate a new key, :shape, with a new value, "egg-like", in our map:
    user=> (assoc favorite-fruit :shape "egg-like")
    {:name "Kiwi", :color "Green", :kcal_per_100g 61, :distinguish_mark "Hairy", :shape "egg-like"}

    The assoc operation returns a new map containing our previous key-value pairs as well as the new association we've just added.

  8. Evaluate favorite-fruit and notice that it remains unchanged:
    user=> favorite-fruit
    {:name "Kiwi", :color "Green", :kcal_per_100g 61, :distinguish_mark "Hairy"}

    Because a map is immutable, the value bound to the favorite-fruit symbol has not changed. By using assoc, we have created a new version of the map.

    Now, the F3C ("Funny Fruity Fruits Consortium") have reverted their previous ruling and determined during their quarterly review of fruit specifications that the color of the kiwi fruit should be brown and not green. To make sure that your application is F3C compliant, you decide to update your system with the new value.

  9. Change the color of favorite-fruit by associating a new value to the :color key:
    user=> (assoc favorite-fruit :color "Brown")
    {:name "Kiwi", :color "Brown", :kcal_per_100g 61, :distinguish_mark "Hairy"}

    assoc replaces the existing value when a key already exists, because HashMaps cannot have duplicate keys.

  10. If we wanted to add more structured information, we could add a map as a value. Add production information as a nested map in our Kiwi map:
    user=> (assoc favorite-fruit :yearly_production_in_tonnes {:china 2025000 :italy 541000 :new_zealand 412000 :iran 311000 :chile 225000})
    {:name "Kiwi", :color "Green", :kcal_per_100g 61, :distinguish_mark "Hairy", :yearly_production_in_tonnes {:china 2025000, :italy 541000, :new_zealand 412000, :iran 311000, :chile 225000}}

    Having nested maps or other data types is commonly used to represent structured information.

    New research has found out that the Kiwi contains fewer calories than previously thought, and to stay compliant, the F3C requires organizations to reduce the current value of kcal per 100 g by 1.

  11. Decrement kcal_per_100g with the assoc function, as follows:
    user=> (assoc favorite-fruit :kcal_per_100g (- (:kcal_per_100g favorite-fruit) 1))
    {:name "Kiwi", :color "Green", :kcal_per_100g 60, :distinguish_mark "Hairy"}

    Great! It works, but there is a more elegant way to deal with this type of operation. When you need to change a value in a map based on a previous value, you can use the update function. While the assoc function lets you associate a completely new value to a key, update allows you to compute a new value based on the previous value of a key. The update function takes a function as its third parameter.

  12. Decrement kcal_per_100g with the update function and dec, as follows:
    user=> (update favorite-fruit :kcal_per_100g dec)
    {:name "Kiwi", :color "Green", :kcal_per_100g 60, :distinguish_mark "Hairy"}

    Notice how the value of :kcal_per_100g changed from 61 to 60.

  13. You can also pass arguments to the function provided to update; for example, if we wanted to lower :kcal_per_100g by 10 instead of 1, we could use the subtract function, -, and write the following:
    user=> (update favorite-fruit :kcal_per_100g - 10)
    {:name "Kiwi", :color "Green", :kcal_per_100g 51, :distinguish_mark "Hairy"}

    Like assoc, update does not change the immutable map; it returns a new map.

    This example illustrates the power of functions being "first-class citizens": we treat them like typical values; in this case, a function was passed as an argument to another function. We will elaborate on this concept in the next chapter while diving into functions in more depth.

  14. Finally, use dissoc (as in "dissociate") to remove one or multiple elements from a map:
    user=> (dissoc favorite-fruit :distinguish_mark)
    {:name "Kiwi", :color "Green", :kcal_per_100g 61}
    user=> (dissoc favorite-fruit :kcal_per_100g :color)
    {:name "Kiwi", :distinguish_mark "Hairy"}

Well done! Now that we know how to use maps, it is time to move on to the next data structure: sets.

Sets

A set is a collection of unique values. Clojure provides HashSet and SortedSet. Hash Sets are implemented as Hash Maps, with the key and the value of each entry being identical.

Hash Sets are fairly common in Clojure and have a literal notation of a hash with curly braces, #{}, for example:

user=> #{1 2 3 4 5}
#{1 4 3 2 5}

Notice in the preceding expression that when the set is evaluated, it does not return the elements of the sets in the order that they were defined in the literal expression. This is because of the internal structure of the HashSet. The value is transformed in a unique hash, which allows fast access but does not keep the insertion order. If you care about the order in which the elements are added, you need to use a different data structure, for example, a sequence such as a vector (which we will soon discover). Use a HashSet to represent elements that logically belong together, for example, an enumeration of unique values.

As with maps, sets cannot have duplicate entries:

user=> #{:a :a :b :c}
Syntax error reading source at (REPL:135:15).
Duplicate key: :a

Hash Sets can be created from a list of values by passing those values to the hash-set function:

user=> (hash-set :a :b :c :d)
#{:c :b :d :a}

Hash Sets can also be created from another collection with the set function. Let's create a HashSet from a vector:

user=> (set [:a :b :c])
#{:c :b :a}

Notice that the order defined in the vector was lost.

The set function will not throw an error when converting a collection of non-unique values to a set with the set function, which can be useful for deduplicating values:

user=> (set ["No" "Copy" "Cats" "Cats" "Please"])
#{"Copy" "Please" "Cats" "No"}

Notice how one of the duplicate strings, "Cats", was silently removed to create a set.

A Sorted Set can be created with the sorted-set function and have no literal syntax as Hash Sets do:

user=> (sorted-set "No" "Copy" "Cats" "Cats" "Please")
#{"Cats" "Copy" "No" "Please"}

Notice that they are printed in the same way as Hash Sets, only the order looks different. Sorted Sets are sorted based on the natural order of elements they contain rather than the order of the arguments provided upon creation. You could instead provide your own sorting function, but we will focus on Hash Sets as they are far more common and useful.

Exercise 2.03: Using Sets

In this exercise, we will use a Hash Set to represent a collection of supported currencies:

Note

A Hash Set is a good choice of data structure for a list of currencies because we typically want to store a collection of unique values and efficiently check for containment. Also, the order of the currencies probably doesn't matter. If you wanted to associate more data to a currency (such as ISO codes and countries), then you would more likely use nested Maps to represent each currency as an entity, keyed by a unique ISO code. Ultimately, the choice of the data structure depends on how you plan to use the data. In this exercise, we simply want to read it, check for containment, and add items to our set.

  1. Start a REPL. Create a set and bind it to the supported-currencies symbol:
    user=> (def supported-currencies #{"Dollar" "Japanese yen" "Euro" "Indian rupee" "British pound"})
    #'user/supported-currencies
  2. As with maps, you can use get to retrieve an entry from a set, which returns the entry passed as a parameter when present in the set. Use get to retrieve an existing entry as well as a missing entry:
    user=> (get supported-currencies "Dollar")
    "Dollar"
    user=> (get supported-currencies "Swiss franc")
    nil
  3. It is likely that you just want to check for containment, and contains? is, therefore, semantically better. Use contains? instead of get to check for containment:
    user=> (contains? supported-currencies "Dollar")
    true
    user=> (contains? supported-currencies "Swiss franc")
    false

    Notice that contains? returns a Boolean and that get returns the lookup value or nil when not found. There is the edge case of looking up nil in a set that will return nil both when found and not found. In that case, contains? is naturally more suitable.

  4. As with maps, sets and keywords can be used as functions to check for containment. Use the supported-currencies set as a function to look up a value in the set:
    user=> (supported-currencies "Swiss franc")
    nil

    "Swiss franc" isn't in the supported-currencies set; therefore, the preceding return value is nil.

  5. If you tried to use the "Dollar" string as a function to look itself up in the set, you would get the following error:
    user=> ("Dollar" supported-currencies)
    Execution error (ClassCastException) at user/eval7 (REPL:1).
    java.lang.String cannot be cast to clojure.lang.IFn

    We cannot use strings as a function to look up a value in a set or a Map. That's one of the reasons why keywords are a better choice in both sets and maps when possible.

  6. To add an entry to a set, use the conj function, as in "conjoin":
    user=> (conj supported-currencies "Monopoly Money")
    #{"Japanese yen" "Euro" "Dollar" "Monopoly Money" "Indian rupee" "British pound"}
  7. You can pass more than one item to the conj function. Try to add multiple currencies to our Hash Set:
    user=> (conj supported-currencies "Monopoly Money" "Gold dragon" "Gil")
    #{"Japanese yen" "Euro" "Dollar" "Monopoly Money" "Indian rupee" "Gold dragon" "British pound" "Gil"}
  8. Finally, you can remove one or more items with the disj function, as in "disjoin":
    user=> (disj supported-currencies "Dollar" "British pound")
    #{"Japanese yen" "Euro" "Indian rupee"}

That's it for sets! If you ever need to, you can find more functions for working with sets in the clojure.set namespace (such as union and intersection), but this is more advanced usage, so let's move on to the next collection: vectors.

Vectors

A vector is another type of collection that is widely used in Clojure. You can think of vectors as powerful immutable arrays. They are collections of values efficiently accessible by their integer index (starting from 0), and they maintain the order of item insertion as well as duplicates.

Use a vector when you need to store and read elements in order, and when you don't mind duplicate elements. For example, a web browser history could be a good candidate, as you might want to easily go back to the recent pages but also remove older elements using a vector's index, and there would likely be duplicate elements in it. A map or a set wouldn't be of much help in that situation, as you don't have a specific key to look up a value with.

Vectors have a literal notation with square brackets ([]):

user=> [1 2 3]
[1 2 3]

Vectors can also be created with the vector function followed by a list of items as arguments:

user=> (vector 10 15 2 15 0)
[10 15 2 15 0]

You can create a vector from another collection using the vec function; for example, the following expression converts a Hash Set to a vector:

user=> (vec #{1 2 3})
[1 3 2]

As with other collections, vectors also can contain different types of values:

user=> [nil :keyword "String" {:answers [:yep :nope]}]
[nil :keyword "String" {:answers [:yep :nope]}]

We can now start practicing.

Exercise 2.04: Using Vectors

In this exercise, we will discover different ways of accessing and interacting with vectors:

  1. Start a REPL. You can look up values in a vector using their index (that is, their position in the collection) with the get function. Try to use the get function with a literal vector:
    user=> (get [:a :b :c] 0)
    :a
    user=> (get [:a :b :c] 2)
    :c
    user=> (get [:a :b :c] 10)
    nil

    Because vectors start at 0-index, :a is at index 0 and :c is at index 2. When the lookup fails, get returns nil.

  2. Let's bind a vector to a symbol to make the practice more convenient:
    user=> (def fibonacci [0 1 1 2 3 5 8])
    #'user/fibonacci
    user=> (get fibonacci 6)
    8
  3. As with maps and sets, you can use the vector as a function to look up items, but for vectors, the parameter is the index of the value in the vector:
    user=> (fibonacci 6)
    8
  4. Add the next two values of the Fibonacci sequence to your vector with the conj function:
    user=> (conj fibonacci 13 21)
    [0 1 1 2 3 5 8 13 21]

    Notice that the items are added to the end of the vector, and the order of the sequence is kept the same.

  5. Each item in the Fibonacci sequence corresponds to the sum of the previous two items. Let's dynamically compute the next item of the sequence:
    user=>
    (let [size (count fibonacci)
           last-number (last fibonacci)
           second-to-last-number (fibonacci (- size 2))]
        (conj fibonacci (+ last-number second-to-last-number)))
    [0 1 1 2 3 5 8 13]

    In the preceding example, we used let to create three local bindings and improve the readability. We used count to calculate the size of a vector, last to retrieve its last element, 8, and finally, we used the fibonacci vector as a function to retrieve the element at index "size - 2" (which is the value 5 at index 5).

In the body of the let block, we used the local binding to add the two last items to the end of the Fibonacci sequence with conj, which returns 13 (which is, indeed, 5 + 8).

Lists

Lists are sequential collections, similar to vectors, but items are added to the front (at the beginning). Also, they don't have the same performance properties, and random access by index is slower than with vectors. We mostly use lists to write code and macros, or in cases when we need a last-in, first-out (LIFO) type of data structure (for example, a stack), which can arguably also be implemented with a vector.

We create lists with the literal syntax, (), but to differentiate lists that represent code and lists that represent data, we need to use the single quote, ':

user=> (1 2 3)
Execution error (ClassCastException) at user/eval211 (REPL:1).
java.lang.Long cannot be cast to clojure.lang.IFn
user=> '(1 2 3)
(1 2 3)
user=> (+ 1 2 3)
6
user=> '(+ 1 2 3)
(+ 1 2 3)

In the preceding examples, we can see that a list that is not quoted with ' throws an error unless the first item of the list can be invoked as a function.

Lists can also be created with the list function:

user=> (list :a :b :c)
(:a :b :c)

To read the first element of a list, use first:

user=> (first '(:a :b :c :d))
:a

The rest function returns the list without its first item:

user=> (rest '(:a :b :c :d))
(:b :c :d)

We will not talk about iterations and recursion yet, but you could imagine that the combination of first and rest is all you need to "walk" or go through an entire list: simply by calling first on the rest of the list over and over again until there's no rest.

You cannot use the get function with a list to retrieve by index. You could use nth, but it is not efficient as the list is iterated or "walked" until it reaches the desired position:

user=> (nth '(:a :b :c :d) 2)
:c

Exercise 2.05: Using Lists

In this exercise, we will practice using lists by reading and adding elements to a to-do list.

  1. Start a REPL and create a to-do list with a list of actions that you need to do, using the list function as follows:
    user=> (def my-todo (list  "Feed the cat" "Clean the bathroom" "Save the world"))
    #'user/my-todo
  2. You can add items to your list by using the cons function, which operates on sequences:
    user=> (cons "Go to work" my-todo)
    ("Go to work" "Feed the cat" "Clean the bathroom" "Save the world")
  3. Similarly, you can use the conj function, which is used because a list is a collection:
    user=> (conj my-todo "Go to work")
    ("Go to work" "Feed the cat" "Clean the bathroom" "Save the world")

    Notice how the order of the parameters is different. cons is available on lists because a list is a sequence, and conj is available to use on lists because a list is a collection. conj is, therefore, slightly more "generic" and also has the advantage of accepting multiple elements as arguments.

  4. Add multiple elements at once to your list by using the conj function:
    user=> (conj my-todo "Go to work" "Wash my socks")
    ("Wash my socks" "Go to work" "Feed the cat" "Clean the bathroom" "Save the world")
  5. Now it's time to catch up with your task. Retrieve the first element in your to-do list with the first function:
    user=> (first my-todo)
    "Feed the cat"
  6. Once done, you can retrieve the rest of your tasks with the rest function:
    user=> (rest my-todo)
    ("Clean the bathroom" "Save the world")

    You could imagine then having to call first on the rest of the list (if you had to develop a fully blown to-do list application). Because the list is immutable, if you keep calling first on the same my-todo list, you will end up with the same element, "Feed the cat", over and over again, and also with a happy but very fat cat.

  7. Finally, you can also retrieve a specific element from the list using the nth function:
    user=> (nth my-todo 2)
    "Save the world"

    However, remember that retrieving an element at a specific position in a list is slower than with vectors because the list has to be "walked" until the nth element. In that case, you might be better off using a vector. One final note about nth is that it throws an exception when the element at position n is not found.

That is all you need to know about lists for now and we can move on to the next section about collection and sequence abstractions.

Collection and Sequence Abstractions

Clojure's data structures are implemented in terms of powerful abstractions. You might have noticed that the operations we used on collections are often similar, but behave differently based on the type of the collection. For instance, get retrieves items from a map with a key, but from a vector with an index; conj adds elements to a vector at the back, but to a list at the front.

A sequence is a collection of elements in a particular order, where each item follows another. Maps, sets, vectors, and lists are all collections, but only vectors and lists are sequences, although we can easily obtain a sequence from a map or a set.

Let's go through a few examples of useful functions to use with collections. Consider the following map:

user=> (def language {:name "Clojure" :creator "Rich Hickey" :platforms ["Java" "JavaScript" ".NET"]})
#'user/language

Use count to get the number of elements in a collection. Each element of this map is a key-value pair; therefore, it contains three elements:

user=> (count language)
3

Slightly more apparent, the following set contains no elements:

user=> (count #{})
0

We can test whether a collection is empty with the empty? function:

user=> (empty? language)
false
user=> (empty? [])
true

A map is not sequential because there is no logical order between its elements. However, we can convert a map to a sequence using the seq function:

user=> (seq language)
([:name "Clojure"] [:creator "Rich Hickey"] [:platforms ["Java" "JavaScript" ".NET"]])

It yielded a list of vectors or tuples, which means that there is now a logical order and we can use sequence functions on this data structure:

user=> (nth (seq language) 1)
[:creator "Rich Hickey"]

A lot of functions just work on collections directly because they can be turned into a sequence, so you could omit the seq step and, for example, call first, rest, or last directly on a map or a set:

user=> (first #{:a :b :c})
:c
user=> (rest #{:a :b :c})
(:b :a)
user=> (last language)
[:platforms ["Java" "JavaScript" ".NET"]]

The value of using sequence functions such as first or rest on maps and sets seems questionable but treating those collections as sequences means that they can then be iterated. Many more functions are available for processing each item of a sequence, such as map, reduce, filter, and so on. We have dedicated entire chapters to learning about those in the second part of the book so that we can stay focused on the other core functions for now.

into is another useful operator that puts elements of one collection into another collection. The first argument for into is the target collection:

user=> (into [1 2 3 4] #{5 6 7 8})
[1 2 3 4 7 6 5 8]

In the preceding example, each element of the #{5 6 7 8} set was added into the [1 2 3 4] vector. The resulting vector is not in ascending order because Hash Sets are not sorted:

user=> (into #{1 2 3 4} [5 6 7 8])
#{7 1 4 6 3 2 5 8}

In the preceding example, the [5 6 7 8] vector was added to the #{1 2 3 4} set. Once again, Hash Sets do not keep insertion order and the resulting set is simply a logical collection of unique values.

A usage example would be, for example, to deduplicate a vector, just put it into a set:

user=> (into #{} [1 2 3 3 3 4])
#{1 4 3 2}

To put items into a map, you would need to pass a collection of tuples representing key-value pairs:

user=> (into {} [[:a 1] [:b 2] [:c 3]])
{:a 1, :b 2, :c 3}

Each item is "conjoined" in the collection, and so it follows the semantic of the target collection for inserting items with conj. Elements are added to a list at the front:

user=> (into '() [1 2 3 4])
(4 3 2 1)

To help you understand (into '() [1 2 3 4]), here is a step-by-step representation of what happened:

user=> (conj '() 1)
(1)
user=> (conj '(1) 2)
(2 1)
user=> (conj '(2 1) 3)
(3 2 1)
user=> (conj '(3 2 1) 4)
(4 3 2 1)

If you want to concatenate collections, concat might be more appropriate than into. See how they behave differently here:

user=> (concat '(1 2) '(3 4))
(1 2 3 4)
user=> (into '(1 2) '(3 4))
(4 3 1 2)

A lot of Clojure functions that operate on sequences will return sequences no matter what the input type was. concat is one example:

user=> (concat #{1 2 3} #{1 2 3 4})
(1 3 2 1 4 3 2)
user=> (concat {:a 1} ["Hello"])
([:a 1] "Hello")

sort is another example. sort can rearrange a collection to order its elements. It has the benefit of being slightly more obvious in terms of why you would want a sequence as a result:

user=> (def alphabet #{:a :b :c :d :e :f})
#'user/alphabet
user=> alphabet
#{:e :c :b :d :f :a}
user=> (sort alphabet)
(:a :b :c :d :e :f)
user=> (sort [3 7 5 1 9])
(1 3 5 7 9)

But what if you wanted a vector as a result? Well, now you know that you could use the into function:

user=> (sort [3 7 5 1 9])
(1 3 5 7 9)
user=> (into [] *1)
[1 3 5 7 9]

It is interesting to note that conj can also be used on maps. For its arguments to be consistent with other types of collections, the new entry is represented by a tuple:

user=> (conj language [:created 2007])
{:name "Clojure", :creator "Rich Hickey", :platforms ["Java" "JavaScript" ".NET"], :created 2007}

Similarly, a vector is an associative collection of key-value pairs where the key is the index of the value:

user=> (assoc [:a :b :c :d] 2 :z)
[:a :b :z :d]

Exercise 2.06: Working with Nested Data Structures

For the purpose of this exercise, imagine that you are working with a little shop called "Sparkling," whose business is to trade gemstones. It turns out that the owner of the shop knows a bit of Clojure, and has been using a Clojure REPL to manage the inventory with some kind of homemade database. However, the owner has been struggling to work with nested data structures, and they require help from a professional: you. The shop won't share their database because it contains sensitive data – they have just given you a sample dataset so that you know about the shape of the data.

The shop owner read a blog post on the internet saying that pure functions are amazing and make for good quality code. So, they asked you to develop some pure functions that take their gemstone database as the first parameter of each function. The owner said you would only get paid if you provide pure functions. In this exercise, we will develop a few functions that will help us understand and operate on nested data structures.

Note

A pure function is a function where the return value is only determined by its input values. A pure function does not have any side effects, which means that it does not mutate a program's state nor generate any kind of I/O.

  1. Open up a REPL and create the following Hash Map representing the sample gemstone database:

    repl.clj

    1  (def gemstone-db {
    2      :ruby {
    3        :name "Ruby"
    4        :stock 480
    5        :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712]
    6        :properties {
    7          :dispersion 0.018
    8          :hardness 9.0
    9          :refractive-index [1.77 1.78]
    10         :color "Red"
    11       }
    12     }

    One of the most popular questions the shop gets from its customers is about the durability of a gem. This can be found in the properties of a gem, at the :hardness key. The first function that we need to develop is durability, which retrieves the hardness of a given gem.

  2. Let's start by using a function we already know, get, with the :ruby gem as an example:
    user=> (get (get (get gemstone-db :ruby) :properties) :hardness)
    9.0

    It works, but nesting get is not very elegant. We could use the map or keywords as functions and see how it improves the readability.

  3. Use the keywords as a function to see how it improves the readability of our code:
    user=> (:hardness (:properties (:ruby gemstone-db)))
    9.0

    This is slightly better. But it's still a lot of nested calls and parentheses. Surely, there must be a better way!

    When you need to fetch data in a deeply nested map such as this one, use the get-in function. It takes a vector of keys as parameters and digs in the map with just one function call.

  4. Use the get-in function with the [:ruby :properties :hardness] vector of parameters to retrieve the deeply nested :hardness key:
    user=> (get-in gemstone-db [:ruby :properties :hardness])
    9.0

    Great! The vector of keys reads left to right and there is no nested expression. It will make our function a lot more readable.

  5. Create the durability function that takes the database and the gem keyword as a parameter and returns the value of the hardness property:
    user=>
    (defn durability
      [db gemstone]
      (get-in db [gemstone :properties :hardness]))
    #'user/durability
  6. Test your newly created function to make sure that it works as expected:
    user=> (durability gemstone-db :ruby)
    9.0
    user=> (durability gemstone-db :moissanite)
    9.5

    Great! Let's move on to the next function.

    Apparently, a ruby is not simply "red" but "Near colorless through pink through all shades of red to a deep crimson." Who would have thought? The owner is now asking you to create a function to update the color of a gem, because they might want to change some other colors too, for marketing purposes. The function needs to return the updated database.

  7. Let's try to write the code to change the color property of a gem. We can try to use assoc:
    user=> (assoc (:ruby gemstone-db) :properties {:color "Near colorless through pink through all shades of red to a deep crimson"})
    {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:color "Near colorless through pink through all shades of red to a deep crimson"}}

    It seems to work but, all the other properties are gone! We replaced the existing Hash Map at the key property with a new Hash Map that contains only one entry: the color.

  8. We could use a trick. Do you remember the into function? It takes a collection and put its values in another collection, like this:
    user=> (into {:a 1 :b 2} {:c 3})
    {:a 1, :b 2, :c 3}

    If we use the update function combined with into, we could obtain the desired result.

  9. Try to use update combined with into to change the :color property of the ruby gem:
    user=> (update (:ruby gemstone-db) :properties into {:color "Near colorless through pink through all shades of red to a deep crimson"})
    {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:dispersion 0.018, :hardness 9.0, :refractive-index [1.77 1.78], :color "Near colorless through pink through all shades of red to a deep crimson"}}

    That's great, but there are two problems with this approach. First, the combination of update and into is not very readable or easy to understand. Second, we wanted to return the entire database, but we just returned the "Ruby" entry. We would have to add another operation to update this entry in the main database, perhaps by nesting another into, reducing readability even further.

    As with get-in, Clojure offers a simpler way of dealing with nested maps: assoc-in and update-in. They work like assoc and update, but take a vector of keys (such as get-in) as a parameter, instead of a single key.

    You would use update-in when you want to update a deeply nested value with a function (for example, to compute the new value with the previous value). Here, we simply want to replace the color with an entirely new value, so we should use assoc-in.

  10. Use assoc-in to change the color property of the ruby gem:
    user=> (assoc-in gemstone-db [:ruby :properties :color] "Near colorless through pink through all shades of red to a deep crimson")
    {:ruby {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:dispersion 0.018, :hardness 9.0, :refractive-index [1.77 1.78], :color "Near colorless through pink through all shades of red to a deep crimson"}}, :emerald {:name "Emerald", :stock 85, :sales [6605 2373 104 4764 9023], :properties {:dispersion 0.014, :hardness 7.5, :refractive-index [1.57 1.58], :color "Green shades to colorless"}}, :diamond {:name "Diamond", :stock 10, :sales [8295 329 5960 6118 4189 3436 9833 8870 9700 7182 7061 1579], :properties {:dispersion 0.044, :hardness 10, :refractive-index [2.417 2.419], :color "Typically yellow, brown or gray to colorless"}}, :moissanite {:name "Moissanite", :stock 45, :sales [7761 3220], :properties {:dispersion 0.104, :hardness 9.5, :refractive-index [2.65 2.69], :color "Colorless, green, yellow"}}}

    Notice how gemstone-db was returned entirely. Can you notice the value that has changed? There is a lot of data, so it is not very obvious. You can use the pprint function to "pretty print" the value.

    Use pprint on the last returned value to improve the readability and make sure that our assoc-in expression behaved as expected. In a REPL, the last returned value can be obtained with *1:

    Figure 2.1: Printing the output to REPL

    Figure 2.1: Printing the output to REPL

    That is much more readable. We will not use pprint everywhere as it takes a lot of extra space, but you should use it.

  11. Create the change-color pure function, which takes three parameters: a database, a gemstone keyword, and a new color. This function updates the color in the given database and returns the new value of the database:
    user=>
    (defn change-color
      [db gemstone new-color]
      (assoc-in gemstone-db [gemstone :properties :color] new-color))
    #'user/change-color
  12. Test that your newly created function behaves as expected:
    user=> (change-color gemstone-db :ruby "Some kind of red")
    {:ruby {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:dispersion 0.018, :hardness 9.0, :refractive-index [1.77 1.78], :color "Some kind of red"}}, :emerald {:name "Emerald", :stock 85, :sales [6605 2373 104 4764 9023], :properties {:dispersion 0.014, :hardness 7.5, :refractive-index [1.57 1.58], :color "Green shades to colorless"}}, :diamond {:name "Diamond", :stock 10, :sales [8295 329 5960 6118 4189 3436 9833 8870 9700 7182 7061 1579], :properties {:dispersion 0.044, :hardness 10, :refractive-index [2.417 2.419], :color "Typically yellow, brown or gray to colorless"}}, :moissanite {:name "Moissanite", :stock 45, :sales [7761 3220], :properties {:dispersion 0.104, :hardness 9.5, :refractive-index [2.65 2.69], :color "Colorless, green, yellow"}}}

    The owner would like to add one last function to record the sale of a gem and update the inventory accordingly.

    When a sale occurs, the shop owner would like to call the sell function with the following arguments: a database, a gemstone keyword, and a client ID. client-id will be inserted in the sales vector and the stock value for that gem will be decreased by one. As with the other functions, the new value of the database will be returned so that the client can handle the update themselves.

  13. We can use the update-in function in combination with dec to decrement (decrease by one) the stock. Let's try it with the diamond gem:
    user=> (update-in gemstone-db [:diamond :stock] dec)
    {:ruby {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:dispersion 0.018, :hardness 9.0, :refractive-index [1.77 1.78], :color "Near colorless through pink through all shades of red to a deep crimson"}}, :emerald {:name "Emerald", :stock 85, :sales [6605 2373 104 4764 9023], :properties {:dispersion 0.014, :hardness 7.5, :refractive-index [1.57 1.58], :color "Green shades to colorless"}}, :diamond {:name "Diamond", :stock 9, :sales [8295 329 5960 6118 4189 3436 9833 8870 9700 7182 7061 1579], :properties {:dispersion 0.044, :hardness 10, :refractive-index [2.417 2.419], :color "Typically yellow, brown or gray to colorless"}}, :moissanite {:name "Moissanite", :stock 45, :sales [7761 3220], :properties {:dispersion 0.104, :hardness 9.5, :refractive-index [2.65 2.69], :color "Colorless, green, yellow"}}}

    The output is not very readable, and it is hard to verify that the value was correctly updated. Another useful command to improve readability in the REPL is the *print-level* option, which can limit the depth of the data structure printed to the terminal.

  14. Use the *print-level* option to set the depth level to 2, and observe how the result is printed:
    user=> (set! *print-level* 2)
    2
    user=> (update-in gemstone-db [:diamond :stock] dec)
    {:ruby {:name "Ruby", :stock 120, :sales #, :properties #}, :emerald {:name "Emerald", :stock 85, :sales #, :properties #}, :diamond {:name "Diamond", :stock 9, :sales #, :properties #}, :moissanite {:name "Moissanite", :stock 45, :sales #, :properties #}} 

    The diamond stock has indeed decreased by 1, from 10 to 9.

  15. We can use the update-in function again, this time in combination with conj and a client-id to add in the sales vector. Let's try an example with the diamond gem and client-id 999:
    user=> (update-in gemstone-db [:diamond :sales] conj 999)
    {:ruby {:name "Ruby", :stock 120, :sales #, :properties #}, :emerald {:name "Emerald", :stock 85, :sales #, :properties #}, :diamond {:name "Diamond", :stock 10, :sales #, :properties #}, :moissanite {:name "Moissanite", :stock 45, :sales #, :properties #}}

    It might have worked, but we cannot see the sales vector as the data has been truncated by the *print-level* option.

  16. Set *print-level* to nil to reset the option, and reevaluate the previous expression:
    user=> (set! *print-level* nil)
    nil
    user=> (update-in gemstone-db [:diamond :sales] conj 999)
    {:ruby {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:dispersion 0.018, :hardness 9.0, :refractive-index [1.77 1.78], :color "Near colorless through pink through all shades of red to a deep crimson"}}, :emerald {:name "Emerald", :stock 85, :sales [6605 2373 104 4764 9023], :properties {:dispersion 0.014, :hardness 7.5, :refractive-index [1.57 1.58], :color "Green shades to colorless"}}, :diamond {:name "Diamond", :stock 10, :sales [8295 329 5960 6118 4189 3436 9833 8870 9700 7182 7061 1579 999], :properties {:dispersion 0.044, :hardness 10, :refractive-index [2.417 2.419], :color "Typically yellow, brown or gray to colorless"}}, :moissanite {:name "Moissanite", :stock 45, :sales [7761 3220], :properties {:dispersion 0.104, :hardness 9.5, :refractive-index [2.65 2.69], :color "Colorless, green, yellow"}}}

    Notice that our diamond sales vector now contains the value 999.

  17. Now let's write our pure function, which combines the two operations (updating the stock and the clients):
    (defn sell
      [db gemstone client-id]
      (let [clients-updated-db (update-in db [gemstone :sales] conj client-id)]
        (update-in clients-updated-db [gemstone :stock] dec)))
  18. Test your newly created function by selling a :moissanite to client-id 123:
    user=> (sell gemstone-db :moissanite 123)
    {:ruby {:name "Ruby", :stock 120, :sales [1990 3644 6376 4918 7882 6747 7495 8573 5097 1712], :properties {:dispersion 0.018, :hardness 9.0, :refractive-index [1.77 1.78], :color "Near colorless through pink through all shades of red to a deep crimson"}}, :emerald {:name "Emerald", :stock 85, :sales [6605 2373 104 4764 9023], :properties {:dispersion 0.014, :hardness 7.5, :refractive-index [1.57 1.58], :color "Green shades to colorless"}}, :diamond {:name "Diamond", :stock 10, :sales [8295 329 5960 6118 4189 3436 9833 8870 9700 7182 7061 1579], :properties {:dispersion 0.044, :hardness 10, :refractive-index [2.417 2.419], :color "Typically yellow, brown or gray to colorless"}}, :moissanite {:name "Moissanite", :stock 44, :sales [7761 3220 123], :properties {:dispersion 0.104, :hardness 9.5, :refractive-index [2.65 2.69], :color "Colorless, green, yellow"}}}

Notice that the sales vector of the moissanite entity now contains the value 123.

In this exercise, we did not really "update" data but merely derived new data structures from others because of their immutability. Even if we work mostly with immutable data types, Clojure offers simple mechanisms that allow you to persist information. In the following activity, you will create a database that can be read and updated with the techniques acquired in this chapter, and we will even provide a helper function to make the database persistent.

Activity 2.01: Creating a Simple In-Memory Database

In this activity, we are going to create our own implementation of an in-memory database. After all, if the "Sparkling" shop owner was able to do it, then it shouldn't be a problem for us!

Our database interface will live in the Clojure REPL. We will implement functions to create and drop tables, as well as to insert and read records.

For the purposes of this activity, we will provide a couple of helper functions to help you maintain the state of the database in memory:

(def memory-db (atom {}))
(defn read-db [] @memory-db)
(defn write-db [new-db] (reset! memory-db new-db))

We use an atom but you don't need to understand how atoms work for now, as they are explained in great detail later in the book. You just need to know that it will keep a reference to our database in memory, and use two helper functions, read-db and write-db, to read and persist a Hash Map in memory.

As guidance, we would like the data structure to have this shape:

{:table-1 {:data [] :indexes {}} :table-2 {:data [] :indexes {}}

For example, if we used our database in a grocery store to save clients, fruits, and purchases, we can imagine that it would contain the data in this manner:

{
  :clients {
    :data [{:id 1 :name "Bob" :age 30} {:id 2 :name "Alice" :age 24}]
    :indexes {:id {1 0, 2 1}}
    },
  :fruits {
    :data [{:name "Lemon" :stock 10} {:name "Coconut" :stock 3}]
    :indexes {:name {"Lemon" 0, "Coconut" 1}}
  },
  :purchases {
    :data [{:id 1 :user-id 1 :item "Coconut"} {:id 1 :user-id 2 :item "Lemon"}]
    :indexes {:id {1 0, 2 1}}
  }
}

Storing data and indexes separately allows multiple indexes to be created without having to duplicate the actual data.

The indexes map stores an association between the index key and its position in the data vector for each index key. In the fruits table, "Lemon" is the first record of the data vector, so the value in the :name index is 0.

These steps will help you perform the activity:

  1. Create the helper functions. You can get the Hash Map by executing the read-db function with no arguments, and write to the database by executing the write-db function with a Hash Map as an argument.
  2. Start by creating the create-table function. This function should take one parameter: the table name. It should add a new key (the table name) at the root of our Hash Map database, and the value should be another Hash Map containing two entries: an empty vector at the data key and an empty Hash Map at the indexes key.
  3. Test that your create-table function works.
  4. Create a drop-table function such that it takes one parameter as well - the table name. It should remove a table, including all its data and indexes from our database.
  5. Test that your drop-table function works.
  6. Create an insert function. This function should take three parameters: table, record, and id-key. The record parameter is a Hash Map, and id-key corresponds to a key in the record map that will be used as a unique index. For now, we will not handle cases when a table does not exist or when an index key already exists in a given table.

    Try to use a let block to divide the work of the insert function in multiple steps:

    In a let statement, create a binding for the value of the database, retrieved with read-db.

    In the same let statement, create a second binding for the new value of the database (after adding the record in the data vector).

    In the same let statement, retrieve the index at which the record was inserted by counting the number of elements in the data vector.

    In the body of the let statement, update the index at id-key and write the resulting map to the database with write-db.

  7. To verify that your insert function works, try to use it multiple times to insert new records.
  8. Create a select-* function that will return all the records of a table passed as a parameter.
  9. Create a select-*-where function that takes three arguments: table-name, field, and field-value. The function should use the index map to retrieve the index of the record in the data vector and return the element.
  10. Modify the insert function to reject any index duplicate. When a record with id-key already exists in the indexes map, we should not modify the database and print an error message to the user.

    On completing the activity, the output should be similar to this:

    user=> (create-table :fruits)
    {:clients {:data [], :indexes {}}, :fruits {:data [], :indexes {}}}
    user=> (insert :fruits {:name "Pear" :stock 3} :name)
    Record with :name Pear already exists. Aborting
    user=> (select-* :fruits)
    [{:name "Pear", :stock 3} {:name "Apricot", :stock 30} {:name "Grapefruit", :stock 6}]
    user=> (select-*-where :fruits :name "Apricot")
    {:name "Apricot", :stock 30}

In this activity, we have used our new knowledge about reading and updating both simple and deeply nested data structures to implement a simple in-memory database. This was not an easy feat – well done!

Note

The solution for this activity can be found via this link.

Summary

In this chapter, we discovered the concept of immutability. We learned about Clojure's simple data types, as well as their implementation on different host platforms. We discovered the most common types of collections and sequences: maps, sets, vectors, and lists. We saw how to use them with generic collections and sequence operations. We learned how to read and update complex structures of nested collections. We also learned about the standard functions for using collection data structures, as well as more advanced usage with deeply nested data structures. In the next chapter, we will learn advanced techniques for working with functions.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Master the tools and patterns of the Clojure and ClojureScript ecosystems
  • Learn the fundamentals of functional programming and immutability
  • Apply your skills practically by developing a range of scalable applications

Description

The Clojure Workshop is a step-by-step guide to Clojure and ClojureScript, designed to quickly get you up and running as a confident, knowledgeable developer. Because of the functional nature of the language, Clojure programming is quite different to what many developers will have experienced. As hosted languages, Clojure and ClojureScript can also be daunting for newcomers because of complexities in the tooling and the challenge of interacting with the host platforms. To help you overcome these barriers, this book adopts a practical approach. Every chapter is centered around building something. As you progress through the book, you will progressively develop the 'muscle memory' that will make you a productive Clojure programmer, and help you see the world through the concepts of functional programming. You will also gain familiarity with common idioms and patterns, as well as exposure to some of the most widely used libraries. Unlike many Clojure books, this Workshop will include significant coverage of both Clojure and ClojureScript. This makes it useful no matter your goal or preferred platform, and provides a fresh perspective on the hosted nature of the language. By the end of this book, you'll have the knowledge, skills and confidence to creatively tackle your own ambitious projects with Clojure and ClojureScript.

Who is this book for?

The Clojure Workshop is for anyone who is curious about functional programming and wants to get started learning Clojure or ClojureScript. Prior experience of another programming language, such as Java or JavaScript, is recommended, and will help you grasp the concepts covered in this book more easily.

What you will learn

  • Write idiomatic code with Clojure and ClojureScript
  • Understand and use common patterns and best practices
  • Experiment with code and interact with programs using the REPL
  • Learn the fundamentals of functional programming and immutability
  • Master concepts including mapping, filtering, reducing and recursion
  • Structure and build your code using namespaces and Leiningen
  • Write unit tests to validate application behavior
  • Simplify your code and improve efficiency with macros
Estimated delivery fee Deliver to Czechia

Premium delivery 7 - 10 business days

€25.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jan 29, 2020
Length: 800 pages
Edition : 1st
Language : English
ISBN-13 : 9781838825485
Category :
Languages :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Estimated delivery fee Deliver to Czechia

Premium delivery 7 - 10 business days

€25.95
(Includes tracking information)

Product Details

Publication date : Jan 29, 2020
Length: 800 pages
Edition : 1st
Language : English
ISBN-13 : 9781838825485
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 95.97
The Go Workshop
€32.99
Hands-On Reactive Programming with Clojure
€29.99
The Clojure Workshop
€32.99
Total 95.97 Stars icon

Table of Contents

15 Chapters
1. Hello REPL! Chevron down icon Chevron up icon
2. Data Types and Immutability Chevron down icon Chevron up icon
3. Functions in Depth Chevron down icon Chevron up icon
4. Mapping and Filtering Chevron down icon Chevron up icon
5. Many to One: Reducing Chevron down icon Chevron up icon
6. Recursion and Looping Chevron down icon Chevron up icon
7. Recursion II: Lazy Sequences Chevron down icon Chevron up icon
8. Namespaces, Libraries and Leiningen Chevron down icon Chevron up icon
9. Host Platform Interoperability with Java and JavaScript Chevron down icon Chevron up icon
10. Testing Chevron down icon Chevron up icon
11. Macros Chevron down icon Chevron up icon
12. Concurrency Chevron down icon Chevron up icon
13. Database Interaction and the Application Layer Chevron down icon Chevron up icon
14. HTTP with Ring Chevron down icon Chevron up icon
15. The Frontend: A ClojureScript UI Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.3
(13 Ratings)
5 star 69.2%
4 star 7.7%
3 star 15.4%
2 star 0%
1 star 7.7%
Filter icon Filter
Top Reviews

Filter reviews by




Sarah Ragsdale Sep 18, 2020
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I've just started using it and have already learned that defn is a macro that combines def with fn (I was wondering why reading other books but this showed it right away with the examples). And that a function's argument list is really just a vector being used and not just some function-specific syntax. Which makes sense but they spelled it out nicely. All of the examples are great practice too.The many examples that walk you thru the code are great - 700 pages' worth. This is a great way to learn the basics of the language with many different exercises. After this, I plan to pick up one of the intermediate level books.
Amazon Verified review Amazon
john near the sea Sep 26, 2020
Full star icon Full star icon Full star icon Full star icon Full star icon 5
many books, not sure why i gravitate to this but it aligns with how i think, personally, it seems.
Amazon Verified review Amazon
Patrick R. Mar 13, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Exemples and exercices are clear and concise, the tone is light but serious and the authors hold your hands concepts after concepts until you are able to write your own programs. Once you finish this book you can actually code Clojure programs and understand most other more theoretical books about Clojure/FP. Clojure is definitely for experienced programmers but this book makes it approachable.
Amazon Verified review Amazon
Szabolcs Baader Apr 08, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Ganz gut!!
Amazon Verified review Amazon
Viswa Viswanathan Apr 28, 2020
Full star icon Full star icon Full star icon Full star icon Full star icon 5
It is one thing to write a book that explains the important concepts well and quite another to create a book that explains well AND ALSO gives the reader well-designed tasks that allow the reader to test their understanding and to solidify and deepen their understanding. The authors have done a superlative job of both of these aspects. The "Exercises" (guided labs that lead the learner through important concepts) and the "Activities" that challenge the learner to use the skills they earned) are all well-designed.Most of us can learn only by immersing ourselves in the subject matter, but most books (even good ones) do not provide enough opportunities for that to happen and the net effect that that many readers are unable to progress from understanding the ideas into the practical realm. I found this book to be outstanding in this regard.Despite its intent to teach the topic from scratch and in depth -- which can be conflicting objectives, the book is thorough and successful in it goals. It takes the reader all the way to topics like testing, web applications, Clojurescript and Reagent.Although I have not finished the book yet, I believe that I will be ready to take on real projects right after I finish this book.Congratulations and many thanks to the authors and the publisher.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela