1. Introduction

Clojure is a functional programming language that runs entirely on the Java Virtual Machine, in a similar way to Scala and Kotlin. Clojure is considered to be a Lisp derivative and will be familiar to anyone who has experience with other Lisp languages.

This tutorial gives an introduction to the Clojure language, introducing how to get started with it and some of the key concepts to how it works.

2. Installing Clojure

Clojure is available as installers and convenience scripts for use on Linux and macOS. Unfortunately, at this stage, Windows doesn’t have such an installer.

However, the Linux scripts might work in something such as Cygwin or Windows Bash. There is also an online service that can be used to test out the language, and older versions have a standalone version that can be used.

2.1. Standalone Download

The standalone JAR file can be downloaded from Maven Central. Unfortunately, versions newer than 1.8.0 no longer work this way easily due to the JAR file having been split into smaller modules.

Once this JAR file is downloaded, we can use it as an interactive REPL simply by treating it as an executable JAR:

$ java -jar clojure-1.8.0.jar
Clojure 1.8.0
user=>

2.2. Web Interface to REPL

A web interface to the Clojure REPL is available at https://repl.it/languages/clojure for us to try without needing to download anything. Currently, this only supports Clojure 1.8.0 and not the newer releases.

2.3. Installer on MacOS

If you use macOS and have Homebrew installed, then the latest release of Clojure can be installed easily:

$ brew install clojure

This will support the latest version of Clojure – 1.10.0 at time of writing. Once installed, we can load the REPL simply by using the clojure or clj commands:

$ clj
Clojure 1.10.0
user=>

2.4. Installer on Linux

A self-installing shell script is available for us to install the tools on Linux:

$ curl -O https://download.clojure.org/install/linux-install-1.10.0.411.sh
$ chmod +x linux-install-1.10.0.411.sh
$ sudo ./linux-install-1.10.0.411.sh

As with the macOS installer, these will be available for the most recent releases of Clojure and can be executed using the clojure or clj commands.

3. Introduction to the Clojure REPL

All of the above options give us access to the Clojure REPL. This is the direct Clojure equivalent of the JShell tool for Java 9 and above and allows us to enter Clojure code and see the result immediately directly. This is a fantastic way to experiment and discover how certain language features work.

Once the REPL is loaded, we’ll have a prompt at which any standard Clojure code can be entered and immediately executed. This includes simple Clojure constructs, as well as interaction with other Java libraries – though they need to be available on the classpath to be loaded.

The prompt of the REPL is an indication of the current namespace we are working in. For the majority of our work, this is the user namespace, and so the prompt will be:

user=>

Everything in the rest of this article will assume that we have access to the Clojure REPL, and will all work directly in any such tool.

4. Language Basics

The Clojure language looks very different from many other JVM based languages, and will possibly seem very unusual to start with. It’s considered to be a dialect of Lisp and has very similar syntax and functionality to other Lisp languages.

A lot of the code that we write in Clojure – as with other Lisp dialects – is expressed in the form of Lists. Lists can then be evaluated to produce results – either in the form of more lists or simple values.

For example:

(+ 1 2) ; = 3

This is a list consisting of three elements. The “+” symbol indicates that we are performing this call – addition. The remaining elements are then used with this call. Thus, this evaluates to “1 + 2”.

By using a List syntax here, this can be trivially extended. For example, we can do:

(+ 1 2 3 4 5) ; = 15

And this evaluates to “1 + 2 + 3 + 4 + 5”.

Note as well the semi-colon character. This is used in Clojure to indicate a comment and isn’t the end of the expression as we’d see in Java.

4.1. Simple Types

Clojure is built on top of the JVM, and as such we have access to the same standard types as any other Java application. Types are typically inferred automatically and don’t need to be specified explicitly.

For example:

123 ; Long
1.23 ; Double
"Hello" ; String
true ; Boolean

We can specify some more complicated types as well, using special prefixes or suffixes:

42N ; clojure.lang.BigInt
3.14159M ; java.math.BigDecimal
1/3 ; clojure.lang.Ratio
#"[A-Za-z]+" ; java.util.regex.Pattern

Note that the clojure.lang.BigInt type is used instead of java.math.BigInteger. This is because the Clojure type has some minor optimizations and fixes.

4.2. Keywords and Symbols

Clojure gives us the concept of both keywords and symbols. Keywords refer only to themselves and are often used for things such as map keys. Symbols, on the other hand, are names used to refer to other things. For example, variable definitions and function names are symbols.

We can construct keywords by using a name prefixed with a colon:

user=> :kw
:kw
user=> :a
:a

Keywords have direct equality with themselves, and not with anything else:

user=> (= :a :a)
true
user=> (= :a :b)
false
user=> (= :a "a")
false

Most other things in Clojure that are not simple values are considered to be symbols. These evaluate to whatever they refer to, whereas a keyword always evaluates to itself:

user=> (def a 1)
#'user/a
user=> :a
:a
user=> a
1

4.3. Namespaces

The Clojure language has the concept of namespaces for organizing our code. Every piece of code we write lives in a namespace.

By default, the REPL runs in the user namespace – as seen by the prompt stating “user=>”.

We can create and change namespaces using the ns keyword:

user=> (ns new.ns)
nil
new.ns=>

Once we’ve changed namespaces, anything that is defined in the old one is no longer available to us, and anything defined in the new one is now available.

We can access definitions across namespaces by fully qualifying them. For example, the namespace clojure.string defines a function upper-case.

If we’re in the clojure.string namespace, we can access it directly. If we’re not, then we need to qualify it as clojure.string/upper-case:

user=> (clojure.string/upper-case "hello")
"HELLO"
user=> (upper-case "hello") ; This is not visible in the "user" namespace
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: upper-case in this context
user=> (ns clojure.string)
nil
clojure.string=> (upper-case "hello") ; This is visible because we're now in the "clojure.string" namespace
"HELLO"

We can also use the require keyword to access definitions from another namespace in an easier way. There are two main ways that we can use this – to define a namespace with a shorter name so that it’s easier to use, and to access definitions from another namespace without any prefix directly:

clojure.string=> (require '[clojure.string :as str])
nil
clojure.string=> (str/upper-case "Hello")
"HELLO"

user=> (require '[clojure.string :as str :refer [upper-case]])
nil
user=> (upper-case "Hello")
"HELLO"

Both of these only affect the current namespace, so changing to a different one will need to have new requires. This helps to keep our namespaces cleaner and give us access to only what we need.

4.4. Variables

Once we know how to define simple values, we can assign them to variables. We can do this using the keyword def:

user=> (def a 123)
#'user/a

Once we’ve done this, we can use the symbol a anywhere we want to represent this value:

user=> a
123

Variable definitions can be as simple or as complicated as we want.

For example, to define a variable as the sum of numbers, we can do:

user=> (def b (+ 1 2 3 4 5))
#'user/b
user=> b
15

Notice that we never have to declare the variable or indicate what type it is. Clojure automatically determines all of this for us.

If we try to use a variable that has not been defined, then we will instead get an error:

user=> unknown
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: unknown in this context
user=> (def c (+ 1 unknown))
Syntax error compiling at (REPL:1:8).
Unable to resolve symbol: unknown in this context

Notice that the output of the def function looks slightly different from the input. Defining a variable a returns a string of ‘user/a. This is because the result is a symbol, and this symbol is defined in the current namespace.

4.5. Functions

We’ve already seen a couple of examples of how to call functions in Clojure. We create a list that starts with the function to be called, and then all of the parameters.

When this list evaluates, we get the return value from the function. For example:

user=> (java.time.Instant/now)
#object[java.time.Instant 0x4b6690c0 "2019-01-15T07:54:01.516Z"]
user=> (java.time.Instant/parse "2019-01-15T07:55:00Z")
#object[java.time.Instant 0x6b8d96d9 "2019-01-15T07:55:00Z"]
user=> (java.time.OffsetDateTime/of 2019 01 15 7 56 0 0 java.time.ZoneOffset/UTC)
#object[java.time.OffsetDateTime 0xf80945f "2019-01-15T07:56Z"]

We can also nest calls to functions, for when we want to pass the output of one function call in as a parameter to another:

user=> (java.time.OffsetDateTime/of 2018 01 15 7 57 0 0 (java.time.ZoneOffset/ofHours -5))
#object[java.time.OffsetDateTime 0x1cdc4c27 "2018-01-15T07:57-05:00"]

Also, we can also define our functions if we desire. Functions are created using the fn command:

user=> (fn [a b]
  (println "Adding numbers" a "and" b)
  (+ a b)
)
#object[user$eval165$fn__166 0x5644dc81 "user$eval165$fn__166@5644dc81"]

Unfortunately, this doesn’t give the function a name that can be used. Instead, we can define a symbol that represents this function using def, exactly as we’ve seen for variables:

user=> (def add
  (fn [a b]
    (println "Adding numbers" a "and" b)
    (+ a b)
  )
)
#'user/add

Now that we’ve defined this function, we can call it the same as any other function:

user=> (add 1 2)
Adding numbers 1 and 2
3

As a convenience, Clojure also allows us to use defn to define a function with a name in a single go.

For example:

user=> (defn sub [a b]
  (println "Subtracting" b "from" a)
  (- a b)
)
#'user/sub
user=> (sub 5 2)
Subtracting 2 from 5
3

4.6. Let and Local Variables

The def call defines a symbol that is global to the current namespace. This is typically not what is desired when executing code. Instead, Clojure offers the let call to define variables local to a block. This is especially useful when using them inside functions, where you don’t want the variables to leak outside of the function.

For example, we could define our sub function:

user=> (defn sub [a b]
  (def result (- a b))
  (println "Result: " result)
  result
)
#'user/sub

However, using this has the following unexpected side effect:

user=> (sub 1 2)
Result:  -1
-1
user=> result ; Still visible outside of the function
-1

Instead, let’s re-write it using let:

user=> (defn sub [a b]
  (let [result (- a b)]
    (println "Result: " result)
    result
  )
)
#'user/sub
user=> (sub 1 2)
Result:  -1
-1
user=> result
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: result in this context

This time the result symbol is not visible outside of the function. Or, indeed, outside of the let block in which it was used.

5. Collections

So far, we’ve been mostly interacting with simple values. We have seen lists as well, but nothing more. Clojure does have a full set of collections that can be used, though, consisting of lists, vectors, maps, and sets:

  • A vector is an ordered list of values – any arbitrary value can be put into a vector, including other collections.
  • A set is an unordered collection of values, and can never contain the same value more than once.
  • A map is a simple set of key/value pairs. It’s very common to use keywords as the keys in a map, but we can use any value we like, including other collections.
  • A list is very similar to a vector. The difference is similar to that between an ArrayList and a LinkedList in Java. Typically, a vector is preferred, but a list is better if we want to be adding elements to the start, or if we only ever want to access the elements in sequential order.

5.1. Constructing Collections

Creating each of these can be done using a shorthand notation or using a function call:

; Vector
user=> [1 2 3]
[1 2 3]
user=> (vector 1 2 3)
[1 2 3]

; List
user=> '(1 2 3)
(1 2 3)
user=> (list 1 2 3)
(1 2 3)

; Set
user=> #{1 2 3}
#{1 3 2}
user=> (hash-set 1 2 3)
#{1 3 2}

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

Notice that the Set and Map examples don’t return the values in the same order. This is because these collections are inherently unordered, and what we see depends on how they are represented in memory.

We can also see that the syntax for creating a list is very similar to the standard Clojure syntax for expressions. A Clojure expression is, in fact, a list that gets evaluated, whereas the apostrophe character here indicates that we want the actual list of values instead of evaluating it.

We can, of course, assign a collection to a variable in the same way as any other value. We can also use one collection as a key or value inside another collection.

Lists are considered to be a seq. This means that the class implements the ISeq interface. All other collections can be converted to a seq using the seq function:

user=> (seq [1 2 3])
(1 2 3)
user=> (seq #{1 2 3})
(1 3 2)
user=> (seq {:a 1 2 3})
([:a 1] [2 3])

5.2. Accessing Collections

Once we have a collection, we can interact with it to get values back out again. How we can do this depends slightly on the collection in question, since each of them has different semantics.

Vectors are the only collection that lets us get any arbitrary value by index. This is done by evaluating the vector and index as an expression:

user=> (my-vector 2) ; [1 2 3]
3

We can do the same, using the same syntax, for maps as well:

user=> (my-map :b)
2

We also have functions for accessing vectors and lists to get the first value, last value, and the remainder of the list:

user=> (first my-vector)
1
user=> (last my-list)
3
user=> (next my-vector)
(2 3)

Maps have additional functions to get the entire list of keys and values:

user=> (keys my-map)
(:a :b)
user=> (vals my-map)
(1 2)

The only real access that we have to sets is to see if a particular element is a member.

This looks very similar to accessing any other collection:

user=> (my-set 1)
1
user=> (my-set 5)
nil

5.3. Identifying Collections

We’ve seen that the way we access a collection varies depending on the type of collection we have. We have a set of functions we can use to determine this, both in a specific and more generic manner.

Each of our collections has a specific function to determine if a given value is of that type – list? for lists, set? for sets, and so on. Additionally, there is seq? for determining if a given value is a seq of any kind, and associative? to determine if a given value allows associative access of any kind – which means vectors and maps:

user=> (vector? [1 2 3]) ; A vector is a vector
true
user=> (vector? #{1 2 3}) ; A set is not a vector
false
user=> (list? '(1 2 3)) ; A list is a list
true
user=> (list? [1 2 3]) ; A vector is not a list
false
user=> (map? {:a 1 :b 2}) ; A map is a map
true
user=> (map? #{1 2 3}) ; A set is not a map
false
user=> (seq? '(1 2 3)) ; A list is a seq
true
user=> (seq? [1 2 3]) ; A vector is not a seq
false
user=> (seq? (seq [1 2 3])) ; A vector can be converted into a seq
true
user=> (associative? {:a 1 :b 2}) ; A map is associative
true
user=> (associative? [1 2 3]) ; A vector is associative
true
user=> (associative? '(1 2 3)) ; A list is not associative
false

5.4. Mutating Collections

In Clojure, as with most functional languages, all collections are immutable. Anything that we do to change a collection results in a brand new collection being created to represent the changes. This can give huge efficiency benefits and means that there is no risk of accidental side effects.

However, we also have to be careful that we understand this, otherwise the expected changes to our collections will not be happening.

Adding new elements to a vector, list, or set is done using conj. This works differently in each of these cases, but with the same basic intention:

user=> (conj [1 2 3] 4) ; Adds to the end
[1 2 3 4]
user=> (conj '(1 2 3) 4) ; Adds to the beginning
(4 1 2 3)
user=> (conj #{1 2 3} 4) ; Unordered
#{1 4 3 2}
user=> (conj #{1 2 3} 3) ; Adding an already present entry does nothing
#{1 3 2}

We can also remove entries from a set using disj. Note that this doesn’t work on a list or vector, because they are strictly ordered:

user=> (disj #{1 2 3} 2) ; Removes the entry
#{1 3}
user=> (disj #{1 2 3} 4) ; Does nothing because the entry wasn't present
#{1 3 2}

Adding new elements to a map is done using assoc. We can also remove entries from a map using dissoc:

user=> (assoc {:a 1 :b 2} :c 3) ; Adds a new key
{:a 1, :b 2, :c 3}
user=> (assoc {:a 1 :b 2} :b 3) ; Updates an existing key
{:a 1, :b 3}
user=> (dissoc {:a 1 :b 2} :b) ; Removes an existing key
{:a 1}
user=> (dissoc {:a 1 :b 2} :c) ; Does nothing because the key wasn't present
{:a 1, :b 2}

5.5. Functional Programming Constructs

Clojure is, at its heart, a functional programming language. This means that we have access to many traditional functional programming concepts – such as map, filter, and reduce.** These generally work the same as in other languages. The exact syntax may be slightly different, though.

Specifically, these functions generally take the function to apply as the first argument, and the collection to apply it to as the second argument:

user=> (map inc [1 2 3]) ; Increment every value in the vector
(2 3 4)
user=> (map inc #{1 2 3}) ; Increment every value in the set
(2 4 3)

user=> (filter odd? [1 2 3 4 5]) ; Only return odd values
(1 3 5)
user=> (remove odd? [1 2 3 4 5]) ; Only return non-odd values
(2 4)

user=> (reduce + [1 2 3 4 5]) ; Add all of the values together, returning the sum
15

6. Control Structures

As with all general purpose languages, Clojure features calls for standard control structures, such as conditionals and loops.

6.1. Conditionals

Conditionals are handled by the if statement. This takes three parameters: a test, a block to execute if the test is true, and a block to execute if the test is false. Each of these can be a simple value or a standard list that will be evaluated on demand:

user=> (if true 1 2)
1
user=> (if false 1 2)
2

Our test can be anything at all that we need – it doesn’t need to be a true/false value. It can also be a block that gets evaluated to give us the value that we need:

user=> (if (> 1 2) "True" "False")
"False"

All of the standard checks, including =, >, and <, can be used here. There’s also a set of predicates that can be used for various other reasons – we saw some already when looking at collections, for example:

user=> (if (odd? 1) "1 is odd" "1 is even")
"1 is odd"

The test can return any value at all – it doesn’t need only to be true or false. However, it is considered to be true if the value is anything except false or nil. This is different from the way that JavaScript works, where there is a large set of values that are considered to be “truth-y” but not true:

user=> (if 0 "True" "False")
"True"
user=> (if [] "True" "False")
"True"
user=> (if nil "True" "False")
"False"

6.2. Looping

Our functional support on collections handles much of the looping work – instead of writing a loop over the collection, we use the standard functions and let the language do the iteration for us.

Outside of this, looping is done entirely using recursion. We can write recursive functions, or we can use the loop and recur keywords to write a recursive style loop:

user=> (loop [accum [] i 0]
  (if (= i 10)
    accum
    (recur (conj accum i) (inc i))
  ))
[0 1 2 3 4 5 6 7 8 9]

The loop call starts an inner block that is executed on every iteration and starts by setting up some initial parameters. The recur call then calls back into the loop, providing the next parameters to use for the iteration. If recur is not called, then the loop finishes.

In this case, we loop every time that the i value is not equal to 10, and then as soon as it is equal to 10, we instead return the accumulated vector of numbers.

7. Summary

This article has given an introduction to the Clojure programming language and shows how the syntax works and some of the things that you can do with it. This is only an introductory level and doesn’t go into the depths of everything that can be done with the language.

However, why not pick it up, give it a go and see what you can do with it.


« 上一篇: Blade - 完全指南