Въведение в Clojure

1. Въведение

Clojure е функционален език за програмиране, който работи изцяло на Java Virtual Machine, по подобен начин на Scala и Kotlin. Clojure се счита за производно на Lisp и ще бъде познато на всеки, който има опит с други езици на Lisp.

Този урок дава въведение в езика Clojure, представя как да започнете с него и някои от ключовите концепции за това как работи.

2. Инсталиране на Clojure

Clojure се предлага като инсталатори и скриптове за удобство за използване на Linux и macOS . За съжаление на този етап Windows няма такъв инсталатор.

Скриптовете на Linux обаче могат да работят в нещо като Cygwin или Windows Bash. Има и онлайн услуга, която може да се използва за тестване на езика , а по-старите версии имат самостоятелна версия, която може да се използва.

2.1. Самостоятелно изтегляне

Самостоятелният JAR файл може да бъде изтеглен от Maven Central. За съжаление, версии по-нови от 1.8.0 вече не работят лесно по този начин поради JAR файла, който е разделен на по-малки модули.

След като този JAR файл бъде изтеглен, можем да го използваме като интерактивен REPL, просто като го третираме като изпълним JAR:

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

2.2. Уеб интерфейс за REPL

Уеб интерфейс за Clojure REPL е достъпен на //repl.it/languages/clojure, за да опитаме, без да е необходимо да изтегляме нищо. Понастоящем това поддържа само Clojure 1.8.0, но не и по-новите версии.

2.3. Инсталатор на MacOS

Ако използвате macOS и имате инсталиран Homebrew, последната версия на Clojure може да бъде инсталирана лесно:

$ brew install clojure

Това ще поддържа най-новата версия на Clojure - 1.10.0 по време на писането. Веднъж инсталиран, можем да заредим REPL просто като използваме командите clojure или clj :

$ clj Clojure 1.10.0 user=>

2.4. Инсталатор на Linux

Наличен е самоинсталиращ се скрипт за черупки за инсталиране на инструментите на Linux:

$ curl -O //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

Както при инсталатора на macOS, те ще бъдат достъпни за най-новите версии на Clojure и могат да бъдат изпълнени с помощта на командите clojure или clj .

3. Въведение в REPL на Clojure

Всички горепосочени опции ни дават достъп до Clojure REPL. Това е прекият еквивалент на Clojure на инструмента JShell за Java 9 и по-нови версии и ни позволява да въведем Clojure код и да видим резултата веднага директно. Това е фантастичен начин да експериментирате и да откриете как работят определени езикови характеристики.

След като REPL бъде зареден, ще получим подкана, при която всеки стандартен код на Clojure може да бъде въведен и незабавно изпълнен. Това включва прости конструкции на Clojure, както и взаимодействие с други библиотеки на Java - въпреки че те трябва да са налични в пътя на класа, за да бъдат заредени.

Подканата на REPL е индикация за текущото пространство на имената, в което работим. За по-голямата част от нашата работа това е потребителското пространство на имената, така че подканата ще бъде:

user=>

Всичко в останалата част на тази статия ще приеме, че имаме достъп до Clojure REPL и ще работи директно във всеки такъв инструмент.

4. Езикови основи

Езикът Clojure изглежда много по-различно от много други базирани на JVM езици и вероятно ще изглежда много необичаен за начало. Смята се, че е диалект на Lisp и има много сходен синтаксис и функционалност с другите езици на Lisp.

Голяма част от кода, който пишем на Clojure - както при другите диалекти на Lisp - се изразява под формата на Списъци . След това списъците могат да бъдат оценени, за да се получат резултати - под формата на повече списъци или прости стойности.

Например:

(+ 1 2) ; = 3

Това е списък, състоящ се от три елемента. Символът „+“ показва, че изпълняваме това повикване - добавяне. След това останалите елементи се използват с това повикване. По този начин това се оценява на „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 requirekeyword 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 aanywhere 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 "[email protected]"]

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

Тази статия е дала въведение в езика за програмиране Clojure и показва как работи синтаксисът и някои от нещата, които можете да правите с него. Това е само уводно ниво и не навлиза в дълбините на всичко, което може да се направи с езика.

Защо обаче не го вземете, опитайте и вижте какво можете да направите с него.