Introducción a Clojure

1. Introducción

Clojure es un lenguaje de programación funcional que se ejecuta completamente en la máquina virtual Java, de manera similar a Scala y Kotlin. Clojure se considera un derivado de Lisp y resultará familiar para cualquiera que tenga experiencia con otros lenguajes Lisp.

Este tutorial brinda una introducción al lenguaje Clojure, presentando cómo comenzar con él y algunos de los conceptos clave de cómo funciona.

2. Instalación de Clojure

Clojure está disponible como instaladores y scripts de conveniencia para usar en Linux y macOS . Desafortunadamente, en esta etapa, Windows no tiene tal instalador.

Sin embargo, los scripts de Linux pueden funcionar en algo como Cygwin o Windows Bash. También hay un servicio en línea que se puede usar para probar el idioma , y las versiones anteriores tienen una versión independiente que se puede usar.

2.1. Descarga independiente

El archivo JAR independiente se puede descargar desde Maven Central. Desafortunadamente, las versiones posteriores a la 1.8.0 ya no funcionan de esta manera fácilmente debido a que el archivo JAR se ha dividido en módulos más pequeños.

Una vez que se descarga este archivo JAR, podemos usarlo como un REPL interactivo simplemente tratándolo como un JAR ejecutable:

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

2.2. Interfaz web para REPL

Una interfaz web para Clojure REPL está disponible en //repl.it/languages/clojure para que la probemos sin necesidad de descargar nada. Actualmente, esto solo es compatible con Clojure 1.8.0 y no con las versiones más recientes.

2.3. Instalador en MacOS

Si usa macOS y tiene Homebrew instalado, entonces la última versión de Clojure se puede instalar fácilmente:

$ brew install clojure

Esto admitirá la última versión de Clojure: 1.10.0 al momento de escribir. Una vez instalado, podemos cargar el REPL simplemente usando los comandos clojure o clj :

$ clj Clojure 1.10.0 user=>

2.4. Instalador en Linux

Hay disponible un script de shell autoinstalable para que instalemos las herramientas en 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

Al igual que con el instalador de macOS, estos estarán disponibles para las versiones más recientes de Clojure y se pueden ejecutar usando los comandos clojure o clj .

3. Introducción a Clojure REPL

Todas las opciones anteriores nos dan acceso a Clojure REPL. Este es el equivalente directo de Clojure de la herramienta JShell para Java 9 y superior y nos permite ingresar el código Clojure y ver el resultado de inmediato y directamente. Esta es una forma fantástica de experimentar y descubrir cómo funcionan ciertas características del lenguaje.

Una vez que se carga el REPL, tendremos un mensaje en el que se puede ingresar y ejecutar inmediatamente cualquier código Clojure estándar. Esto incluye construcciones simples de Clojure, así como la interacción con otras bibliotecas de Java, aunque deben estar disponibles en la ruta de clases para cargarlas.

El mensaje de REPL es una indicación del espacio de nombres actual en el que estamos trabajando. Para la mayor parte de nuestro trabajo, este es el espacio de nombres de usuario , por lo que el mensaje será:

user=>

Todo en el resto de este artículo asumirá que tenemos acceso a Clojure REPL, y todo funcionará directamente en dicha herramienta.

4. Conceptos básicos del idioma

El lenguaje Clojure se ve muy diferente de muchos otros lenguajes basados ​​en JVM, y posiblemente parecerá muy inusual para empezar. Se considera un dialecto de Lisp y tiene una sintaxis y funcionalidad muy similar a otros lenguajes Lisp.

Gran parte del código que escribimos en Clojure, como con otros dialectos Lisp, se expresa en forma de listas . Las listas se pueden evaluar para producir resultados, ya sea en forma de más listas o valores simples.

Por ejemplo:

(+ 1 2) ; = 3

Esta es una lista que consta de tres elementos. El símbolo "+" indica que estamos realizando esta llamada - suma. Los elementos restantes se utilizan luego con esta llamada. Por lo tanto, esto se evalúa como "1 + 2".

Al usar una sintaxis de lista aquí, esto se puede extender trivialmente . Por ejemplo, podemos hacer:

(+ 1 2 3 4 5) ; = 15

Y esto se evalúa como "1 + 2 + 3 + 4 + 5".

Tenga en cuenta también el carácter de punto y coma. Esto se usa en Clojure para indicar un comentario y no es el final de la expresión como veríamos en Java.

4.1. Tipos simples

Clojure está construido sobre la JVM y, como tal, tenemos acceso a los mismos tipos estándar que cualquier otra aplicación Java . Por lo general, los tipos se infieren automáticamente y no es necesario especificarlos explícitamente.

Por ejemplo:

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

También podemos especificar algunos tipos más complicados, usando prefijos o sufijos especiales:

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

Este artículo ofrece una introducción al lenguaje de programación Clojure y muestra cómo funciona la sintaxis y algunas de las cosas que puede hacer con ella. Este es solo un nivel introductorio y no profundiza en todo lo que se puede hacer con el idioma.

Sin embargo, ¿por qué no recogerlo, darle una oportunidad y ver qué puede hacer con él?