Cheatsheet Clojure
Lisp moderno na JVM: funcional, imutável e concorrente
Clojure
Básico
Formas y S-expressions
; Todo es una lista en notación prefija: (+ 1 2 3) ; 6 (* 2 3 4) ; 24 (str "Hola" " " "Mundo") ; "Hola Mundo" ; La primera posición es la función: (- 10 3) ; 7 (/ 22 7) ; 22/7 (¡ratio!)
Clojure usa S-expressions: código entre paréntesis en notación prefija, donde el primer elemento es la función y el resto son los argumentos.
cond y case
; cond: múltiples condiciones (cond (neg? x) "negativo" (zero? x) "cero" :else "positivo") ; case: valor constante (rápido) (case dia 1 "lunes" 2 "martes" "otro") ; default
cond comprueba varios predicados en secuencia, con :else como default. case compara contra valores constantes y es más rápido, pero exige una rama default.
Keywords y symbols
; Keywords (auto-evaluadas, usadas como claves):
:nombre
:edad
(:nombre {:nombre "Ana"}) ; "Ana"
; Keywords con namespace:
::local ; :mi.ns/local
:user/nombre
; Symbols (se refieren a vars):
'mi-funcion
+ ; el símbolo de la función +
(quote x) ; equivale a 'xUn keyword (empieza con :) se evalúa a sí mismo y se usa mucho como clave de mapa. Un symbol se refiere a una var; el quote (') impide su evaluación.
Tipos de datos
42 ; entero (Long) 3.14 ; flotante (Double) 22/7 ; ratio (¡racional exacto!) "texto" ; string \a ; char true false ; boolean nil ; null :keyword ; keyword 'simbolo ; symbol
Los tipos principales incluyen Long, Double, Ratio (racionales exactos), String, Char, Boolean, nil, keywords y symbols. Todos son inmutables.
Operaciones aritméticas
(+ 1 2) ; 3 (- 10 3) ; 7 (* 4 5) ; 20 (/ 22 7) ; 22/7 (ratio) (double (/ 22 7)) ; 3.142... (mod 10 3) ; 1 (resto) (inc 5) ; 6 (dec 5) ; 4 (quot 10 3) ; 3 (cociente)
Los operadores son funciones prefijas: + - * /. La división entera devuelve un Ratio exacto; usa double para obtener decimal. inc/dec suman/restan 1.
do y comentarios
; do ejecuta varias formas en secuencia: (do (println "paso 1") (println "paso 2") (+ 1 2)) ; devuelve 3 (última forma) ; Comentarios: ; esto es un comentario de línea #_ (forma ignorada por el reader) (+ 1 #_ 2 3) ; 4 (el 2 es ignorado)
do ejecuta varias expresiones y devuelve el valor de la última. Los comentarios usan ;; el macro #_ hace que el reader ignore por completo la forma siguiente.
def y let
; def crea un binding global (var):
(def nombre "Clojure")
(def pi 3.14159)
; let crea bindings locales:
(let [x 10
y 20]
(+ x y)) ; 30
; let con reutilización:
(let [base 5
doble (* base 2)]
(+ base doble)) ; 15def define una variable global (una var). let crea bindings locales visibles solo dentro de su cuerpo — es la forma preferida para valores temporales.
Comparación y lógicos
; Comparación (¡encadenable!): (= 1 1) ; true (< 1 2 3) ; true (1<2<3) (>= 5 5 3) ; true ; Lógicos: (and true true) ; true (or false true) ; true (not true) ; false ; Predicados útiles: (nil? nil) ; true (even? 4) ; true
Las comparaciones (= < > <= >=) aceptan varios argumentos encadenados. Los lógicos son and, or y not. Las funciones terminadas en ? son predicados que devuelven booleanos.
if y when
; if: (if test then else) (if (> x 0) "positivo" "negativo o cero") ; when: solo la rama verdadera (when (pos? x) (println "¡Es positivo!")) ; if devuelve un valor (es una expresión): (def signo (if (neg? x) -1 1))
if es una expresión que devuelve un valor: (if test then else). when ejecuta el cuerpo solo si el test es verdadero y devuelve nil en caso contrario.
Strings
; Concatenar: (str "Hola" " " "Mundo") ; "Hola Mundo" ; Biblioteca clojure.string: (require '[clojure.string :as str]) (str/upper-case "hola") ; "HOLA" (str/lower-case "HOLA") ; "hola" (str/trim " hola ") ; "hola" (str/split "a,b,c" #",") ; ["a" "b" "c"] (str/join "-" [1 2 3]) ; "1-2-3"
La función str concatena y convierte valores en texto. El namespace clojure.string ofrece operaciones como upper-case, trim, split y join.
Funções
defn
; Definir una función: (defn sumar [a b] (+ a b)) (sumar 3 5) ; 8 ; Con docstring: (defn saludo "Devuelve un saludo" [nombre] (str "Hola, " nombre "!")) (saludo "Ana") ; "Hola, Ana!"
defn define una función con nombre. El docstring opcional la documenta y es visible con (doc saludo) en el REPL.
Shorthand #()
; #() es azúcar para fn: #(* % 2) ; (fn [x] (* x 2)) #(+ %1 %2) ; dos argumentos #(apply + %&) ; argumentos variádicos ; Uso práctico: (map #(* % 2) [1 2 3]) ; (2 4 6) (filter #(> % 3) [1 2 3 4 5]) ; (4 5) ; ¡Atención: no anidar #()!
La sintaxis #() es una forma corta de función anónima. % es el argumento, %1/%2 para varios y %& para el resto. No debe anidarse.
Threading macros
; -> inserta como 1er argumento:
(-> " hola "
(clojure.string/trim)
(clojure.string/upper-case)) ; "HOLA"
; ->> inserta como último argumento:
(->> (range 10)
(filter even?)
(map #(* % %))
(reduce +)) ; 120
; some-> para nil-safe:
(some-> obj :campo :subcampo)Las threading macros evitan paréntesis anidados. -> pasa el resultado como primer argumento de cada forma; ->> como último. some-> se detiene si encuentra nil.
Arity overloading
; Varias aridades (nº de args):
(defn area
([r] (* Math/PI r r)) ; círculo
([l a] (* l a))) ; rectángulo
(area 2) ; 12.56...
(area 3 4) ; 12
; Aridad con recursión:
(defn sumar-hasta
([n] (sumar-hasta n 0))
([n acc]
(if (zero? n) acc
(recur (dec n) (+ acc n)))))Una función puede tener varias aridades (números de argumentos), cada una con su cuerpo. Es común que una aridad corta delegue en otra más completa con un valor inicial.
partial y comp
; partial: aplica argumentos iniciales (def doble (partial * 2)) (doble 5) ; 10 (def saludar (partial str "Hola, ")) (saludar "Ana") ; "Hola, Ana" ; comp: compone funciones (derecha->izquierda) (def transformar (comp inc #(* % 2))) (transformar 3) ; 7 (inc (2*3))
partial fija los primeros argumentos de una función, creando una nueva. comp compone funciones de derecha a izquierda: (comp f g) es f(g(x)).
Funciones variádicas
; & recoge el resto de los argumentos: (defn sumar-todos [& nums] (apply + nums)) (sumar-todos 1 2 3 4) ; 10 ; Mezclar fijos y variádicos: (defn log [nivel & msgs] (println nivel (apply str msgs))) (log "INFO" "a" "b") ; INFO ab
& recoge los argumentos restantes en una secuencia. Permite funciones con número variable de argumentos, como sumar-todos que acepta cualquier cantidad.
apply
; apply llama a una función con una seq de args: (apply + [1 2 3]) ; 6 (apply max [3 7 2]) ; 7 ; Args fijos + colección: (apply str "a" ["b" "c"]) ; "abc" ; Sin apply vs con apply: (+ [1 2 3]) ; ERROR (apply + [1 2 3]) ; 6
apply llama a una función pasando los elementos de una colección como argumentos individuales. Es esencial para usar funciones como + o max con una lista.
Funciones anónimas (fn)
; fn crea una función anónima: (fn [x] (* x 2)) ; Uso directo: ((fn [x] (* x 2)) 5) ; 10 ; Como argumento: (map (fn [x] (* x x)) [1 2 3]) ; (1 4 9) ; Con nombre local (recursión): ((fn fact [n] (if (<= n 1) 1 (* n (fact (dec n))))) 5)
fn crea una función anónima. Puede tener un nombre interno (para recursión) y es útil cuando la función se usa una sola vez, por ejemplo en map o filter.
loop y recur
; loop/recur: recursión en tail-call
(defn factorial [n]
(loop [i n
acc 1]
(if (<= i 1)
acc
(recur (dec i) (* acc i)))))
(factorial 10) ; 3628800loop define puntos de recursión y recur salta de vuelta sin acumular stack (tail-call optimization). Es la forma idiomática de hacer bucles en Clojure.
Coleções
Vectors
; Vector: ordenado, acceso por índice (def v [1 2 3 4 5]) (nth v 0) ; 1 (v 2) ; 3 (como función) (count v) ; 5 (conj v 6) ; [1 2 3 4 5 6] (assoc v 0 99) ; [99 2 3 4 5] (get v 10 :default) ; :default
Un vector ([]) es una colección ordenada con acceso eficiente por índice. conj añade al final y assoc sustituye una posición, devolviendo siempre un nuevo vector.
Sets
; Set: colección sin duplicados
(def s #{1 2 3 4 5})
(contains? s 3) ; true
(s 3) ; 3 (o nil)
(conj s 6) ; #{1 2 3 4 5 6}
(disj s 3) ; #{1 2 4 5}
; Operaciones (clojure.set):
(require '[clojure.set :as set])
(set/union #{1 2} #{2 3}) ; #{1 2 3}
(set/intersection #{1 2} #{2 3}) ; #{2}Un set (#{}) guarda valores únicos sin orden. conj añade, disj elimina y contains? comprueba pertenencia. El namespace clojure.set da union e intersection.
Colecciones y secuencias
; seq convierte cualquier colección en secuencia:
(seq [1 2 3]) ; (1 2 3)
(seq {:a 1}) ; ([:a 1])
(seq "abc") ; (\a \b \c)
; Probar vacío (idiomático):
(if (seq coll)
"tiene datos"
"vacío")
; (empty? coll) también funcionaCualquier colección puede verse como una seq (secuencia) con seq. Es idiomático probar (seq coll) para verificar si tiene datos, pues devuelve nil si está vacía.
Lists
; List: acceso eficiente al inicio (def l '(1 2 3)) (first l) ; 1 (rest l) ; (2 3) (cons 0 l) ; (0 1 2 3) (conj l 0) ; (0 1 2 3) (¡al inicio!) ; Las listas son lazy-friendly: (map inc l) ; (2 3 4)
Una list ('()) está optimizada para acceder y añadir al inicio. conj añade al frente (al contrario que el vector). Son ideales para secuencias lazy.
Destructuring de vectors
; Extraer posiciones: (let [[a b c] [1 2 3]] (+ a b c)) ; 6 ; Primero y resto: (let [[x & resto] [1 2 3 4]] [x resto]) ; [1 (2 3 4)] ; Ignorar posiciones: (let [[_ segundo _] [10 20 30]] segundo) ; 20
El destructuring de vectors extrae valores por posición dentro de un let o parámetro. & recoge el resto y _ ignora posiciones no usadas.
Maps
; Map: pares clave-valor
(def persona {:nombre "Ana" :edad 30})
; Acceso:
(:nombre persona) ; "Ana"
(get persona :edad) ; 30
(get persona :email "N/A") ; "N/A"
; keywords como función:
(:edad persona) ; 30Un map ({}) guarda pares clave-valor. Las keywords funcionan como funciones de acceso: (:nombre persona). get acepta un valor default si la clave no existe.
Destructuring de maps
; :keys extrae claves keyword:
(let [{:keys [nombre edad]} persona]
(str nombre " - " edad))
; Con default y rename:
(let [{:keys [nombre]
:or {nombre "Anónimo"}} datos]
nombre)
; En parámetros de función:
(defn mostrar [{:keys [nombre edad]}]
(println nombre edad))El destructuring de maps con :keys extrae valores de claves keyword a variables locales. :or define defaults y funciona también en parámetros de funciones.
Actualizar maps
(def persona {:nombre "Ana" :edad 30})
; assoc: añade/sustituye clave
(assoc persona :email "ana@mail.com")
; update: aplica función al valor
(update persona :edad inc) ; edad 31
; dissoc: elimina clave
(dissoc persona :edad)
; merge: combina maps
(merge persona {:ciudad "Lisbon"})Los maps son inmutables: assoc añade o sustituye, update aplica una función al valor de una clave, dissoc elimina y merge combina. Todos devuelven un nuevo map.
Estructuras anidadas
(def datos
{:user {:nombre "Ana"
:direccion {:ciudad "Porto"}}})
; get-in: accede a un camino anidado
(get-in datos [:user :direccion :ciudad])
; "Porto"
; assoc-in: actualiza un camino
(assoc-in datos [:user :edad] 30)
; update-in: aplica una función en el camino
(update-in datos [:user :nombre] clojure.string/upper-case)Para estructuras anidadas, get-in lee un camino de claves, assoc-in define un valor en ese camino y update-in le aplica una función — todo de forma inmutable.
Sequências
map, filter, reduce
; map: transforma cada elemento (map inc [1 2 3]) ; (2 3 4) (map #(* % %) [1 2 3]) ; (1 4 9) ; filter: mantiene los que pasan (filter even? (range 10)) ; (0 2 4 6 8) ; reduce: agrega en un valor (reduce + [1 2 3 4]) ; 10 (reduce + 100 [1 2 3]) ; 106 (valor inicial)
map aplica una función a cada elemento, filter mantiene los que satisfacen el predicado y reduce combina todo en un único valor con un acumulador.
for comprehension
; Combina varios generadores:
(for [x (range 3)
y (range 3)]
[x y])
; Con filtro :when:
(for [x (range 10)
:when (even? x)]
(* x x)) ; (0 4 16 36 64)
; Con binding :let:
(for [x (range 5)
:let [y (* x 2)]]
[x y])for es una list comprehension que combina generadores. :when filtra elementos y :let crea bindings intermedios. Devuelve una secuencia lazy.
some, every?, not-any?
; some: primer valor verdadero
(some even? [1 3 4 5]) ; true
(some #{3} [1 2 3]) ; 3
; every?: ¿todos pasan?
(every? pos? [1 2 3]) ; true
(every? even? [2 4 5]) ; false
; not-any?: ¿ninguno pasa?
(not-any? neg? [1 2 3]) ; true
; not-empty: nil si vacía
(not-empty []) ; nilsome devuelve el primer valor verdadero del predicado (o nil). every? comprueba si todos pasan y not-any? si ninguno pasa. Son predicados de secuencia.
take, drop, first, rest
(take 3 [1 2 3 4 5]) ; (1 2 3) (drop 2 [1 2 3 4 5]) ; (3 4 5) (take-while pos? [3 2 1 0 -1]) ; (3 2 1) (drop-while pos? [3 2 1 0 -1]) ; (0 -1) (first [1 2 3]) ; 1 (last [1 2 3]) ; 3 (rest [1 2 3]) ; (2 3) (nth [1 2 3] 1) ; 2
Estas funciones cortan secuencias: take/drop retiran N elementos, take-while/drop-while usan un predicado. first, last y rest acceden a partes.
Transducers
; Componer transformaciones sin colecciones intermedias:
(def xform
(comp
(filter even?)
(map #(* % %))
(take 5)))
; Aplicar de varias formas:
(into [] xform (range 100)) ; [0 4 16 36 64]
(transduce xform + (range 100)) ; 56
(sequence xform (range 100))Un transducer es una transformación reutilizable e independiente de la colección. Compuesto con comp, se aplica con into, transduce o sequence sin crear colecciones intermedias.
Lazy sequences
; range es lazy (evalúa bajo demanda):
(def nums (range)) ; ¡infinita!
(take 5 nums) ; (0 1 2 3 4)
; lazy-seq crea una seq perezosa:
(defn fib-seq
([] (fib-seq 0 1))
([a b]
(lazy-seq
(cons a (fib-seq b (+ a b))))))
(take 10 (fib-seq))
; (0 1 1 2 3 5 8 13 21 34)Las lazy sequences solo calculan sus elementos cuando son necesarios. lazy-seq crea secuencias perezosas, permitiendo estructuras infinitas consumidas con take.
concat, interleave, partition
; concat: une secuencias (concat [1 2] [3 4]) ; (1 2 3 4) ; interleave: alterna elementos (interleave [:a :b :c] [1 2 3]) ; (:a 1 :b 2 :c 3) ; partition: agrupa en bloques (partition 2 [1 2 3 4 5]) ; ((1 2) (3 4)) (partition-all 2 [1 2 3 4 5]) ; ((1 2) (3 4) (5)) ; flatten: elimina anidamiento (flatten [[1 2] [3 [4]]]) ; (1 2 3 4)
concat une secuencias, interleave alterna elementos de varias y partition agrupa en bloques de N. flatten elimina todos los niveles de anidamiento.
range, repeat, cycle
; range: secuencia numérica
(range 5) ; (0 1 2 3 4)
(range 2 10) ; (2 3 ... 9)
(range 0 10 2) ; (0 2 4 6 8) (paso)
; repeat: repite un valor
(take 4 (repeat "x")) ; ("x" "x" "x" "x")
; cycle: repite la colección
(take 6 (cycle [1 2 3])) ; (1 2 3 1 2 3)
; repeatedly: llama a una función
(take 3 (repeatedly rand))range genera secuencias numéricas (con inicio, fin y paso). repeat repite un valor, cycle repite una colección y repeatedly llama a una función varias veces.
sort, group-by, frequencies
; sort / sort-by:
(sort [3 1 2]) ; (1 2 3)
(sort-by :edad personas) ; ordena por clave
; group-by: agrupa por función
(group-by even? [1 2 3 4])
; {false (1 3), true (2 4)}
; frequencies: cuenta ocurrencias
(frequencies [:a :b :a :c :a])
; {:a 3, :b 1, :c 1}
; distinct: elimina duplicados
(distinct [1 1 2 3 3]) ; (1 2 3)sort ordena y sort-by ordena por una función o clave. group-by agrupa elementos en un map, frequencies cuenta ocurrencias y distinct elimina duplicados.
Concorrência
Atoms
; Estado mutable atómico y síncrono: (def contador (atom 0)) ; Leer (deref o @): @contador ; 0 (deref contador) ; 0 ; Actualizar con swap!: (swap! contador inc) ; 1 (swap! contador + 10) ; 11 ; Reset (sustituye): (reset! contador 0)
Un atom guarda estado mutable de forma atómica y síncrona. Se lee con @ o deref; se actualiza con swap! (aplica función) o reset! (sustituye).
alter y commute
(def cuenta (ref 100)) ; alter: aplica función (el orden importa) (dosync (alter cuenta - 30)) ; commute: para operaciones conmutativas ; (más rápido, sin orden garantizado) (dosync (commute cuenta + 50)) ; ref-set: sustituye el valor (dosync (ref-set cuenta 0))
Dentro de dosync, alter aplica una función manteniendo el orden, mientras que commute está optimizado para operaciones conmutativas (como +). ref-set sustituye el valor.
swap! y validación
; swap! aplica una función de forma atómica: (def saldo (atom 100)) (swap! saldo - 30) ; 70 ; Con argumentos extra: (swap! saldo + 50) ; 120 ; Validator (rechaza valores inválidos): (def edad (atom 0 :validator pos?)) (swap! edad inc) ; 1 (ok) (reset! edad -5) ; ERROR (falla validator)
swap! aplica una función al valor del atom de forma atómica, reintentando en caso de conflicto. Un :validator rechaza actualizaciones que devuelvan valores inválidos.
Agents
; Actualización asíncrona en cola: (def logger (agent [])) ; send encola una acción: (send logger conj "Evento 1") (send logger conj "Evento 2") ; Leer (puede estar desactualizado): @logger ; Esperar conclusión: (await logger) ; send-off para acciones bloqueantes (IO)
Un agent gestiona estado actualizado de forma asíncrona. send encola acciones procesadas una a una; await espera la conclusión. Usa send-off para operaciones de IO.
Watchers
; add-watch observa cambios:
(def contador (atom 0))
(add-watch contador :log
(fn [key ref antiguo nuevo]
(println antiguo "->" nuevo)))
(swap! contador inc) ; imprime: 0 -> 1
(swap! contador inc) ; imprime: 1 -> 2
; Eliminar:
(remove-watch contador :log)Un watcher es una función llamada siempre que un atom (o ref/agent) cambia. Recibe la clave, la referencia, el valor antiguo y el nuevo. Se elimina con remove-watch.
Futures
; future: cálculo asíncrono (def resultado (future (Thread/sleep 2000) (* 42 42))) ; Hacer otra cosa mientras tanto... ; @ bloquea hasta tener el valor: @resultado ; 1764 ; deref con timeout: (deref resultado 5000 :timeout) (realized? resultado) ; true
Un future ejecuta un cálculo en otra thread y devuelve de inmediato. El @ (deref) bloquea hasta que el valor esté listo. El deref con timeout devuelve un valor si expira.
Refs (STM)
; Refs: estado coordinado transaccional (def cuenta-a (ref 1000)) (def cuenta-b (ref 0)) ; Transacción con dosync: (dosync (alter cuenta-a - 100) (alter cuenta-b + 100)) @cuenta-a ; 900 @cuenta-b ; 100
Las refs permiten actualizar varios valores de forma coordinada vía Software Transactional Memory. Dentro de dosync, los cambios son atómicos — o todos ocurren o ninguno.
Promises
; promise: valor entregado una vez (def p (promise)) ; En una thread, entregar: (deliver p "valor") ; En otra, esperar: @p ; "valor" ; realized? comprueba si ya fue entregado: (realized? p) ; true ; Útil para sincronizar threads
Una promise es un canal de sentido único: se entrega un valor una única vez con deliver y quien hace deref espera hasta que exista. Sirve para sincronizar threads.
Avançado
Macros
; Macro: código que genera código
(defmacro unless [cond & body]
`(if (not ~cond)
(do ~@body)))
(unless false
(println "¡Ejecuta!"))
; Ver la expansión:
(macroexpand '(unless true (println "x")))Un macro transforma código en tiempo de compilación, permitiendo crear nuevas formas de sintaxis. macroexpand muestra el código generado, útil para depurar macros.
Multimethods
; defmulti: dispatch por función
(defmulti area :tipo)
(defmethod area :círculo [{:keys [radio]}]
(* Math/PI radio radio))
(defmethod area :rectangulo [{:keys [ancho alto]}]
(* ancho alto))
(area {:tipo :círculo :radio 2}) ; 12.56
(area {:tipo :rectangulo :ancho 3 :alto 4}) ; 12Un multimethod hace dispatch dinámico según el resultado de una función. defmulti define la función de dispatch y cada defmethod maneja un valor específico.
Syntax quote
; ` = syntax quote (con namespace) ; ~ = unquote (evalúa expresión) ; ~@ = unquote-splicing (inserta seq) `(if (not ~cond) (do ~@body)) ; Ejemplo: (let [x 5] `(+ ~x 1)) ; (clojure.core/+ 5 1) ; ~@ esparce una lista: (let [args '(1 2 3)] `(+ ~@args)) ; (+ 1 2 3)
En syntax quote, ` crea un template de código, ~ (unquote) inserta el valor de una expresión y ~@ (unquote-splicing) esparce una secuencia de argumentos.
reify
; reify: crea un objeto anónimo con protocolos
(def descriptor
(reify Describible
(describir [this] "objeto anónimo")))
(describir descriptor) ; "objeto anónimo"
; Implementar interfaces Java:
(reify Comparable
(compareTo [this otro] 0))reify crea un objeto anónimo que implementa protocols o interfaces Java, sin definir un tipo nombrado. Es útil para implementaciones puntuales y rápidas.
Protocols
; Definir un protocolo (interface): (defprotocol Describible (describir [this])) ; Implementar para tipos: (extend-protocol Describible String (describir [s] (str "String: " s)) Long (describir [n] (str "Número: " n))) (describir "hola") ; "String: hola" (describir 42) ; "Número: 42"
Un protocol define un conjunto de funciones (como una interface). extend-protocol lo implementa para tipos existentes, permitiendo polimorfismo por tipo de forma eficiente.
Vars y dynamic binding
; ^:dynamic permite re-binding temporal: (def ^:dynamic *impresora* :pantalla) (defn log [msg] (println *impresora* msg)) ; binding cambia solo dentro del bloque: (binding [*impresora* :fichero] (log "guardado")) ; :fichero guardado (log "normal") ; :pantalla normal
Una var marcada ^:dynamic puede re-asignarse temporalmente con binding, visible solo dentro del bloque. Por convención, el nombre va entre asteriscos.
defrecord
; Record: tipo de datos nombrado
(defrecord Persona [nombre edad])
; Crear:
(def p (->Persona "Ana" 30))
(map->Persona {:nombre "Ray" :edad 25})
; Acceso (como map):
(:nombre p) ; "Ana"
; Record + protocolo:
(defrecord Animal [nombre]
Describible
(describir [a] (str "Animal: " (:nombre a))))Un defrecord crea un tipo de datos con campos nombrados, accesible como un map. Los constructores ->Nombre y map->Nombre se generan automáticamente, y puede implementar protocols.
Excepciones
; try/catch/finally:
(try
(/ 1 0)
(catch ArithmeticException e
(println "Error:" (.getMessage e)))
(finally
(println "siempre ejecuta")))
; Lanzar excepción:
(throw (ex-info "inválido" {:código 400}))
; try+ (slingshot) maneja mapastry envuelve código que puede fallar, catch maneja la excepción por tipo y finally siempre se ejecuta. throw lanza excepciones; ex-info crea excepciones con datos.
IO e Java Interop
Llamar métodos Java
; .método llama a un método de instancia: (.toUpperCase "hola") ; "HOLA" (.length "texto") ; 5 (.replace "a-b" "-" "+") ; "a+b" ; Clase/método estático: (Math/abs -42) ; 42 (Math/sqrt 16) ; 4.0 (System/currentTimeMillis) ; Campo estático: Math/PI ; 3.14159...
La interop con Java usa .método para métodos de instancia y Clase/método para estáticos. Los campos estáticos se acceden con Clase/CAMPO, como Math/PI.
Escribir ficheros (writer)
(require '[clojure.java.io :as io])
; Escribir con writer:
(with-open [w (io/writer "salida.txt")]
(.write w "primera línea\n")
(.write w "segunda línea\n"))
; Añadir:
(with-open [w (io/writer "log.txt"
:append true)]
(.write w "nueva entrada\n"))io/writer crea un writer para escritura controlada. with-open lo cierra automáticamente al final. La opción :append true añade al fichero existente.
Crear objetos e import
; El punto final crea una instancia: (java.util.Date.) (java.util.ArrayList.) ; Con argumentos: (java.util.ArrayList. 10) ; Import para nombres cortos: (import '[java.time LocalDate]) (LocalDate/now) ; doto: configura un objeto con métodos (doto (java.util.HashMap.) (.put "a" 1) (.put "b" 2))
El punto final (Clase.) crea una instancia. import permite usar nombres cortos. El macro doto llama a varios métodos sobre el mismo objeto, útil para configurar objetos Java mutables.
Entrada y salida estándar
; Imprimir:
(println "Hola" "Mundo") ; con newline
(print "sin newline")
(prn {:a 1}) ; formato legible (edn)
; Leer del stdin:
(def linea (read-line))
(println "Dijiste:" linea)
; printf formateado:
(printf "Nombre: %s, Edad: %d%n" "Ana" 30)println imprime con salto de línea y print sin él. prn imprime en formato edn (legible por Clojure). read-line lee una línea del stdin.
slurp y spit
; slurp: lee un fichero entero a string (def contenido (slurp "datos.txt")) ; spit: escribe un string en un fichero (spit "salida.txt" "Hola Mundo") ; Añadir (append): (spit "log.txt" "línea\n" :append true) ; También funciona con URLs: ; (slurp "https://ejemplo.com")
slurp lee todo el contenido de un fichero a un String y spit escribe un String en un fichero. La opción :append true añade en vez de sobrescribir.
Namespaces y require
; Declarar namespace:
(ns mi-app.core
(:require [clojure.string :as str]
[clojure.set :as set]
[mi-app.utils :refer [ayuda]]))
; Usar:
(str/upper-case "hola") ; con alias
(set/union #{1} #{2})
(ayuda) ; refer directo
; En el REPL:
(require '[clojure.string :as str])ns declara el namespace y sus dependencias. :require carga namespaces, :as define un alias y :refer importa funciones específicas para uso directo.
Leer ficheros (reader)
(require '[clojure.java.io :as io])
; Leer línea a línea:
(with-open [r (io/reader "datos.txt")]
(doseq [linea (line-seq r)]
(println linea)))
; Leer todo como seq de líneas:
(with-open [r (io/reader "datos.txt")]
(doall (line-seq r)))El namespace clojure.java.io da acceso a readers y writers. with-open garantiza el cierre del recurso; line-seq devuelve una secuencia lazy de líneas.
Ferramentas
Clojure CLI y deps.edn
# Comandos principales:
clojure -M -m mi-app.core ; ejecutar
clojure -A:dev ; con alias
clojure -T:build uber ; build
# deps.edn define dependencias:
{:deps
{org.clojure/clojure
{:mvn/version "1.11.1"}
ring/ring-core
{:mvn/version "1.10.0"}}}El Clojure CLI usa el fichero deps.edn para declarar dependencias y aliases. El comando clojure -M ejecuta el programa; los aliases activan configuraciones extra.
Tests
(ns mi-app.core-test
(:require [clojure.test :refer
[deftest is testing]]
[mi-app.core :refer [sumar]]))
(deftest test-sumar
(testing "suma básica"
(is (= 5 (sumar 2 3)))
(is (= 0 (sumar -1 1)))))
; Ejecutar: clojure -M:test (o lein test)Los tests usan clojure.test: deftest define un test, is comprueba una aserción y testing agrupa con una descripción. Se ejecutan con lein test o vía la CLI.
Leiningen
# Crear proyecto: lein new app mi-proyecto # Comandos principales: lein run ; ejecutar lein repl ; REPL lein test ; tests lein uberjar ; JAR ejecutable # project.clj define dependencias: ; :dependencies ; [[org.clojure/clojure "1.11.1"]]
Leiningen es una herramienta de build clásica. lein new crea proyectos, lein run ejecuta y lein uberjar genera un JAR autónomo. Las dependencias van en project.clj.
Buenas prácticas
; Prefiere datos inmutables y funciones puras:
(defn total [items]
(reduce + (map :precio items)))
; Usa keywords como funciones de acceso:
(map :nombre personas)
; Threading para pipelines legibles:
(->> datos
(filter :activo)
(map :valor)
(reduce +))
; Los nombres de predicados terminan en ?:
(defn válido? [x] (pos? x))Prioriza datos inmutables, funciones puras y keywords como accesores. Usa threading macros para pipelines legibles y termina los nombres de predicados en ? y las funciones de mutación en !.
REPL
# Iniciar: clojure ; o: lein repl ; Comandos útiles en el REPL: (doc map) ; documentación (source map) ; código fuente (find-doc "lazy"); buscar docs (apropos "map") ; símbolos con "map" ; Recargar namespace: (require 'mi-app.core :reload)
El REPL es el entorno interactivo de Clojure. Funciones como doc, source, find-doc y apropos ayudan a explorar la biblioteca estándar en tiempo real.
Estructura de proyecto
mi-proyecto/
deps.edn ; dependencias
src/
mi_app/
core.clj ; ns mi-app.core
utils.clj ; ns mi-app.utils
test/
mi_app/
core_test.clj
resources/ ; ficheros estáticos
; _ en el fichero = - en el namespace:
; mi_app/core.clj -> mi-app.coreUn proyecto organiza el código en src/ (con namespaces) y test/. La ruta del fichero define el namespace: los underscores (_) en el fichero se convierten en guiones (-) en el nombre.