cv.cm

Programación · Lenguajes de programación

Tutorial de Clojure para principiantes

7 min de lectura

Respuesta corta

Clojure es un Lisp dinámico y funcional que se ejecuta en la Máquina Virtual de Java (con ClojureScript para JavaScript). El código se escribe como listas entre paréntesis, los datos son inmutables por defecto y los programas se construyen combinando pequeñas funciones sobre vectores, mapas y conjuntos. Instalas la CLI de Clojure, inicias un REPL con clj y ejecutas un archivo con clojure -M hello.clj.

Abrir Probador de regex

Qué es Clojure y quién lo usa

Clojure lo creó Rich Hickey y se publicó por primera vez en 2007. Es un dialecto de Lisp: cada expresión es una lista cuyo primer elemento es la función o el operador, por ejemplo (+ 1 2). Compila a bytecode de la JVM, así que puede llamar directamente a cualquier biblioteca de Java. ClojureScript compila el mismo lenguaje a JavaScript para navegadores y Node.js.

Clojure se usa en servicios web de backend, pipelines de procesamiento de datos y sistemas internos de empresas, sobre todo donde los equipos valoran los datos inmutables y el desarrollo interactivo. Quienes lo usan suelen trabajar dentro de un REPL (bucle de lectura-evaluación-impresión) conectado al editor, evaluando una expresión a la vez mientras el programa se ejecuta.

Instala Clojure y ejecuta Hola, mundo

Instala un entorno de ejecución de Java (JDK 17 o posterior es una opción segura) y luego las herramientas CLI de Clojure desde clojure.org (Homebrew en macOS, un script de instalación en Linux o el instalador de Windows). La CLI ofrece dos comandos: clj (un REPL con edición de línea) y clojure (para ejecutar programas).

;; hello.clj
(println "Hello, world!")
clojure -M hello.clj
# Hello, world!
clj
# user=> (+ 1 2)
# 3

Los proyectos reales guardan sus dependencias en un archivo deps.edn y su código bajo src/, con un espacio de nombres por archivo. Leiningen (lein) es una herramienta de compilación más antigua pero aún popular, con un archivo project.clj.

Valores, tipos y nombres

Clojure tiene tipado dinámico. def enlaza un nombre global (un var) y let enlaza nombres locales. Los valores no cambian después de crearse: no hay instrucción de asignación.

TipoEjemploNotas
Entero42Un long de 64 bits; 42N es un BigInt
Coma flotante3.14Un double de Java
Razón(/ 1 3)Fracción exacta 1/3, no 0.333
Cadena"hello"Un String de Java, solo con comillas dobles
Carácter\aUn solo carácter
Keyword:nameEtiqueta rápida que se evalúa a sí misma, a menudo clave de un mapa
Booleano / niltrue, false, nilSolo false y nil son falsos
(def pi-approx 3.14159)
(let [r 2
      area (* pi-approx r r)]
  (println "Area:" area))
;; Area: 12.56636
(str "a" 1 :b)   ;; => "a1:b"

Control de flujo

En Clojure todo es una expresión que devuelve un valor, incluido if. if toma exactamente una forma «then» y una «else»; usa when para varios efectos secundarios sin rama else y cond para varias condiciones.

(defn grade [score]
  (cond
    (>= score 90) "A"
    (>= score 80) "B"
    :else         "C"))
(println (grade 85))            ;; B
(when (pos? 5) (println "positive") :done)
(doseq [x [1 2 3]] (println (* x x)))   ;; 1 4 9
(for [x (range 5) :when (odd? x)] (* x 10))   ;; => (10 30)
(loop [i 0 acc 0]
  (if (< i 5) (recur (inc i) (+ acc i)) acc))   ;; => 10

No hay un bucle for con un contador que modifiques. doseq ejecuta efectos secundarios para cada elemento, for construye una secuencia perezosa (una lista por comprensión) y loop/recur expresa la iteración sin hacer crecer la pila de llamadas.

Funciones

Define funciones con nombre mediante defn. Una función puede tener varias aridades (distinto número de argumentos), y #(...) es una forma corta para funciones anónimas pequeñas en la que % es el argumento.

(defn greet
  ([] (greet "world"))
  ([name] (str "Hello, " name "!")))
(greet)          ;; => "Hello, world!"
(greet "Ana")    ;; => "Hello, Ana!"
(defn sum-all [& nums] (reduce + nums))
(sum-all 1 2 3)  ;; => 6
(map #(* % %) [1 2 3])        ;; => (1 4 9)
(filter even? (range 10))     ;; => (0 2 4 6 8)

Las funciones son valores: puedes pasarlas a map, filter y reduce, devolverlas desde otras funciones y guardarlas en mapas.

Colecciones: listas, vectores, mapas y conjuntos

Las cuatro colecciones básicas de Clojure son inmutables y persistentes: «cambiar» una devuelve una versión nueva que comparte la mayor parte de su estructura con la anterior, así que las actualizaciones son baratas.

ColecciónLiteralUso típico
Lista'(1 2 3)Código y datos secuenciales a los que se añade por el frente
Vector[1 2 3]Secuencia indexada, se añade por el final
Mapa{:name "Ana" :age 30}Registros clave-valor
Conjunto#{:red :green}Valores únicos, pruebas de pertenencia
(def user {:name "Ana" :age 30})
(:name user)                   ;; => "Ana"
(assoc user :email "[email protected]")   ;; new map with :email
(update user :age inc)         ;; => {:name "Ana", :age 31}
(conj [1 2] 3)                 ;; => [1 2 3]
(contains? #{:red :green} :red) ;; => true
(get-in {:a {:b 1}} [:a :b])   ;; => 1
user                           ;; unchanged: {:name "Ana", :age 30}

Características distintivas: macros de enhebrado, átomos y macros

Las macros de enhebrado hacen que los pipelines de datos se lean de arriba abajo. -> inserta el resultado anterior como primer argumento de la siguiente forma; ->> lo inserta como último argumento.

(->> (range 1 11)
     (filter odd?)
     (map #(* % %))
     (reduce +))   ;; => 165
(def counter (atom 0))
(swap! counter inc)
(swap! counter + 10)
@counter           ;; => 11
(defmacro unless [test & body]
  `(if ~test nil (do ~@body)))
(unless false (println "runs"))

Los átomos guardan un estado que puede cambiar: swap! aplica una función de forma atómica, lo que permite usarlos con seguridad desde varios hilos. Los refs y los agents cubren otros casos de concurrencia. Como el código son datos (listas), las macros pueden transformar el código antes de compilarlo; when, cond y -> son en sí mismas macros.

Manejo de errores

Clojure usa las excepciones de la JVM con try, catch y finally. Para adjuntar datos a un error, lanza ex-info y recupera los datos con ex-data.

(defn parse-age [s]
  (let [n (Long/parseLong s)]
    (when (neg? n)
      (throw (ex-info "Age must not be negative" {:value n})))
    n))
(try
  (parse-age "-5")
  (catch clojure.lang.ExceptionInfo e
    (println (ex-message e) (ex-data e)))
  (catch NumberFormatException _
    (println "not a number")))
;; Age must not be negative {:value -5}

Un programa pequeño y completo: frecuencias de palabras

(ns wordcount
  (:require [clojure.string :as str]))
(defn word-counts [text]
  (->> (str/split (str/lower-case text) #"\W+")
       (remove str/blank?)
       frequencies
       (sort-by val >)))
(doseq [[word n] (take 2 (word-counts "The cat and the hat and the bat"))]
  (println word n))
;; the 3
;; and 2

El programa convierte el texto a minúsculas, lo divide por caracteres que no son de palabra con el literal de expresión regular #"\W+", cuenta cada palabra con frequencies (que devuelve un mapa) y ordena las entradas por cantidad en orden descendente.

Siguientes pasos

  • Las guías oficiales en clojure.org: Getting Started, Learn Clojure y la guía de deps.edn.
  • Las páginas de referencia de Clojure sobre la sintaxis del lector, las formas especiales y las estructuras de datos.
  • Configura un editor con un REPL conectado (por ejemplo Calva para VS Code, CIDER para Emacs o Cursive para IntelliJ).
  • Prueba ClojureScript si quieres apuntar al navegador.

Preguntas frecuentes

¿Clojure es un lenguaje funcional?

Sí. Clojure favorece las funciones puras y los datos inmutables, pero es pragmático: permite efectos secundarios y ofrece estado mutable gestionado mediante átomos, refs y agents.

¿Necesito saber Java para aprender Clojure?

No. Puedes aprender el lenguaje sin Java, pero necesitas tener instalado un entorno de ejecución de Java, y saber un poco de la JVM ayuda al leer trazas de pila o usar bibliotecas de Java.

¿Cuál es la diferencia entre una lista y un vector en Clojure?

Una lista '(1 2 3) añade elementos por el frente y se usa sobre todo para código; un vector [1 2 3] añade por el final y permite un acceso rápido por índice. La mayoría de los datos de los programas de Clojure se guardan en vectores y mapas.

¿Por qué Clojure usa tantos paréntesis?

El código de Clojure se escribe como estructuras de datos de Lisp, así que cada llamada es una lista. Esta sintaxis uniforme es lo que hace posibles las macros, y los editores con edición estructural facilitan el manejo de los paréntesis.

¿Cuál es la diferencia entre Clojure y ClojureScript?

Son el mismo lenguaje en anfitriones distintos: Clojure se ejecuta en la JVM y ClojureScript compila a JavaScript. La interoperabilidad con el anfitrión es distinta, pero las funciones básicas y las estructuras de datos son las mismas.

Todos los tutoriales de programación