Tutorial Compojure III: Usando atoms como modelo de datos

En los últimos posts de este tutorial para desarrollar una aplicación web usando compojure hemos visto cómo crear la aplicación con leiningen y la forma de generar vistas usando Hiccup. En este post empezaremos a dotar de algo de funcionalidad a nuestra aplicación creando el modelo interno que nos permita gestionar la colección de héroes y facts.

Normalmente cualquier aplicación necesita almacenar datos de forma persistente y en clojure existen librerías para emplear todo tipo de bases de datos, pero en este caso vamos a limitarnos a mantener los datos en memoria, lo que nos va a permitir explorar una de las alternativas que ofrece clojure para trabajar con estado mutable: los atoms.

Cómo funcionan los atoms

Como buen lenguaje funcional, clojure promueve el uso de estructuras de datos inmutables, pero hay veces en que no queda más remedio que asumir la mutabilidad. Clojure cuenta con opciones para gestionar la mutabilidad de una forma ordenada, eficiente, y segura desde el punto de vista de la programación concurrente.

Un atom nos permite almacenar información mutable y realizar modificaciones sobre ella garantizando que estas modificaciones se ejecutan de forma atómica. El API básica para manejar atoms es muy simple:

; Para definir un atom se utiliza la forma especial atom
; indicando el contenido del atom (en este caso, un map vacío)
(def my-atom (atom {}))

; Para obtener el valor del atom en un momento determinado,
; se utiliza la función deref o @
(println (deref my-atom))
; => {}
(println @my-atom)
; => {}

; Para modificar el valor de un atom, se usa la función
; swap! indicando la función encargada de calcular el 
; nuevo valor a partir del valor anterior del atom. 
; Por ejemplo, para añadir al map una clave :id 
; asociada al valor 1, usaríamos:
(swap! my-atom (fn [a] (assoc a :id 1))

; O en versión más corta:
(swap! my-atom assoc :id 1) 

; Por último, podemos cambiar el valor del atom sin tener
; en cuenta el valor actual con reset!
(reset! my-atom {:id 1})

La función swap! garantiza que la modificación sobre el valor del atom se ejecuta de forma síncrona y atómica, evitando carreras críticas (race conditions).

El funcionamiento de este sistema se nos queda un poco lejos de este tutorial, pero hay una cosa que debemos tener en cuenta, y es que si se producen conflictos entre dos hebras, la función que pasamos a swap! puede ser ejecutada más de una vez, por lo que deberemos asegurarnos de que es una función pura, es decir, sin efectos colaterales (side effects).

El modelo de la aplicación

El modelo de la aplicación que vamos a crear es muy sencillo. Vamos a seguir la línea habitual en clojure de usar estructuras de datos sin esquema prefijado y almacenaremos la información como un map en el que las claves serán los nombres de los héroes y los valores vectores con los facts.

Usando un atom como los que acabamos de ver mantendremos una referencia a un map con ese aspecto que iremos modificando según se vayan añadiendo nuevos facts a los héroes

Para que todo quede ordenadito, creamos el fichero en ./src/compojure_sample/model.clj con el siguiente contenido:

(ns compojure-sample.model)

(def heroes (atom 
  {"Chuck Norris" 
   ["Chuck Norris no te pisa un pie, sino el cuello."
    "Chuck Norris borró la papelera de reciclaje."]
	  
   "Bruce Scheneier"
   ["Science is defined as mankinds futile attempt at learning Bruce Schneiers private key."
    "Others test numbers to see whether they are prime. Bruce decides whether a number is prime."]

   "Arturo Pérez-Reverte"
   ["Pérez-Reverte se baja música en casa de Ramoncín."
    "Pérez-Reverte no necesita investigar para escribir novela histórica, el pasado cambia conforme teclea en la máquina."]}))

Que me perdonen los puristas pero la tabulación del código clojure todavía presenta algunos misterios para mi.

Puede parecer un poco raro usar strings en lugar de keywords como claves del map, pero cuando integremos esto con la parte cliente en javascript nos va a facilitar la vida.

Suele ser una buena idea encapsular al máximo la parte mutable de un programa funcional, y para ello vamos a crear 3 funciones que nos permitan trabajar con el modelo:

(defn get-facts [hero] 
  (get @heroes hero))

(defn add-fact! [hero fact]
	(swap! heroes update-in [hero] conj fact))
		  
(defn get-heroes []
  (sort (keys @heroes)))

Estas funciones nos permiten obtener los facts de un héroe concreto, añadir un nuevo fact a un héroe y obtener los nombres de todos los héroes para los que tenemos facts registrados. La función add-fact! sigue la convención habitual en clojure de acabar con una exclamación (!) aquellas funciones que modifican estado.

Usando la función get-heroes podemos modificar el handler que usamos para mostrar la página principal del sitio y, en lugar de pasarle a la función que genera la vista un vector con valores hardcoded, obtener los nombres de los héroes de nuestro modelo:

(defroutes app-routes
  (GET "/" [] 
    (view/index (model/get-heroes)))
  ...)

¿Qué nos queda?

Ya nos queda poco para acabar de implementar la aplicación de ejemplo. En el próximo post de la serie veremos cómo exponer una especie de API REST basado en JSON para visualizar los facts asociados a un héroe y añadir nuevos facts.

El código fuente completo de este tutorial lo puedes encontrar en mi cuenta en github: https://github.com/jmhdez/compojure-sample. Siéntete libre de clonarlo, cambiarlo y jugar con él todo lo que quieras, y si te animas a realizar alguna corrección, estaré encantado de recibirla.

4 comentarios en “Tutorial Compojure III: Usando atoms como modelo de datos

  1. Quizá uno de los aspectos más bonitos de Clojure (compartido con Haskell, y que lo diferencia de otros lenguajes funcionales) es que insertar la noción de cambio mediante «tipos referenciales» (atoms, refs, agents) no cambia el modelo de programación en absoluto – no es digamos una «trampa» a la que tengamos que recurrir porque el paradigma sea insuficiente. Los datos, los valores, siguen siendo inmutables :)

    Por supuesto le he echado un buen vistazo al resultado final en github. Me encanta cuando la longitud del código correspone al tamaño del problema :) Igualmente, resulta muy fácil razonar código que consume librerías en lugar de seguir un framework.
    No es que no tenga sus desventajas, pero en sus 5 años de existencia, la comunidad de Clojure ha demostrado que es factible estructurar webapps de esta manera.

  2. Desde mi punto de vista, la introducción de referencias sí que supone una (pequeña y necesaria) salida del paradigma. Es cierto que la estructura de datos se mantiene inmutable,pero cargas con la mutabilidad a la referencia.

    Dejando de lado cuestiones teóricas, me gusta más el pragmátismo de clojure para aceptar conceptos como la mutabilidad o la entrada/salida que la forma purista que recuerdo de Haskell con mónadas y más mónadas (al menos hace 10 años, cuando lo usé por última vez :)

    El código la verdad es que no tiene mucho misterio, pero teniendo en cuenta lo críptico que resulta un lenguaje como clojure para la gente, me alegro de que sea así. De todas formas, si lo comparas con el código javascript del tutorial de express no hay mucha diferencia entre un lenguaje y otro (más allá de la sintaxis, claro).

  3. De algún modo hay que representar el cambio! Y el elegido está en consonancia con el modelo del mundo real – no se puede «cambiar el pasado» por ejemplo.

    Las veces que he explorado Node me ha dado justamente la impresión de que tampoco «tiran» particularmente las frameworks en esa comunidad a día de hoy. Y mira si se llevan frameworks en Javascript de lado de cliente…

    Pero para el resto de lenguajes diría sí que es raro desarrollar para la web eligiendo uno cada librería.

  4. En realidad, tanto express/node como compojure/ring, yo los clasificaría como frameworks. Ligeros y personalizables, pero frameworks al fin y al cabo.

    Para mi la principal diferencia entre framework y librería está en quién llama a quien. Al usar una librería tu código invoca funciones de la librería, mientras que al usar un framework escribes código que será invocado en algún momento por el framework (hay un punto de inversión de control).

    En lo que sí coincido es que tanto node como ring parecen especialmente pensados para poder elegir qué librerías incluir como parte del framework a la hora de tratar con conceptos básicos como generación de HTML, autenticación, etc. Esto por ejemplo es mucho menos frecuente en .net, donde frameworks como ASPNET MVC te dan prácticamente todo hecho y es poco habitual ver aplicaciones que cambien componentes estándar (aunque es posible).

    Es una cuestión cultural.

Comentarios cerrados.