Ahora que ya sabemos cómo instalar Clojure y hemos visto un poco de la sintaxis de Clojure, ha llegado el momento de intentar hacer algo útil.
El problema
Hace poco he tenido que resolver un problema real (muy simple, pero real) y he aprovechado para intentar resolverlo con clojure. El problema que tenía era el siguiente:
Tengo dos aplicaciones que comparten gran parte de los textos y una de ellas está traducida a varios idiomas, pero la otra todavía no. Los textos se almacenan en ficheros con un formato muy simple:
100=Aceptar 101=Cancelar 102=Abrir 103=Cerrar
Por cada idioma que soporta la aplicación, existe un fichero con los textos asociados a ese idioma:
app1.es.txt
← textos en español de la aplicación 1app1.fr.txt
← textos en francés de la aplicación 1app2.es.txt
← textos en español de la aplicación 2
Lo que quiero es comprobar qué textos se han mantenido exactamente igual en español (tanto el texto como el identificador de recurso) entre las dos aplicaciones, y generar un nuevo fichero app2.fr.txt
con los textos en francés correspondientes. Con un ejemplo queda más claro:
app1.es.txt | app2.es.txt | app1.fr.txt | app2.fr.txt |
---|---|---|---|
1=Aceptar | 1=Aceptar | 1=OK | 1=OK |
2=Cancelar | 2=Cancelar | 2=Annuler | 2=Annuler |
3=Eliminar | 3=Imprimir | 3=Supprimer | |
… | … | … | … |
Parseando los ficheros
OJO: No soy, ni de lejos, un experto en Clojure. Lo único que pretendo con este post es mostrar el aspecto que tiene un programa en Clojure. Si sabes algo de Clojure, seguramente verás un montón de cosas mejorables; en ese caso, te estaré eternamente agradecido si me lo explicas en los comentarios.
Para parsear un fichero con el formato anterior, podemos usar el siguiente código:
(ns com.koalite (:require [clojure.string :as str])) (defn parse-file [filename] (let [content (slurp filename) lines (str/split content #"\r\n")] (map #(str/split % #"=") lines))) (def es-orig (parse-file "app1.es.txt")) (def es-new (parse-file "app2.es.txt")) (def fr-orig (parse-file "app1.fr.txt"))
Vamos a analizarlo poco a poco:
(ns com.koalite (:require [clojure.string :as str]))
Con esto se define un espacio de nombres, en este caso com.koalite
, y se indica que dentro de ese espacio de nombre se pueden utilizar todo lo que esté definido en el espacio de nombres clojure.string
.
Al importar un espacio de nombres es necesario incluir un alias que se usará para referirse a lo que esté definido en su interior y evitar así colisiones. Por ejemplo, para referenciar la función split
de clojure.string
, usaremos la sintaxis str/split
.
En realidad, esto sería muy parecido a los using
de C#. Sin embargo, ya vamos viendo una de las cosas que comentamos en el post anterior y que es importante recordar; en clojure todo se define con la misma sintaxis: usando listas. No hay una sintaxis especial para definir un espacio de nombres o un using
, se usa una lista y ya está.
Seguimos:
(defn parse-file [filename] (let [content (slurp filename) lines (str/split content #"\r\n")] (map #(str/split % #"=") lines)))
Este código define una función para parsear un fichero con el formato que hemos visto anteriormente. Recibe como parámetro el nombre del fichero y el cuerpo de la función está formado únicamente por una forma especial: let
.
La forma especial let
nos permite definir una serie de «variables» locales al cuerpo del let
, indicándolas en un vector (recordad que un vector se define con corchetes en lugar de paréntesis). Cada elemento del vector es una variable que toma el valor del elemento que le sigue, es decir, en este caso hemos definido dos variables locales:
content
, que es el resultado de usar la funciónslurp
sobre el fichero.slurp
devuelve el contenido completo del fichero como unstring
.lines
, que es resultado de aplicar la funciónsplit
(incluida en el espacio de nombresclojure.string
, por eso se usa el aliasstr/
delante al referenciarla) al contenido del fichero (la variable que hemos declarado antes) y la expresión regular «\r\n». Con esto tendremos en la variablelines
un vector con las líneas del fichero.
Es importante señalar que estas variables no son como una variable normal de C# o Java. Las variables declaradas en el let
son inmutables y por tanto no pueden ser modificadas en el cuerpo del let
.
A continuación tenemos el cuerpo del let
, donde estamos usando la función map
(equivalente a un Select
de Linq) para convertir cada línea del fichero en un vector con dos elementos, el primero de ellos es el identificador del recurso y el segundo el texto del recurso.
El primer argumento de map
es la función que aplicaremos a cada elemento de la colección pasada como segundo parámetro de map
. En este caso estamos definiendo una función anónima con la sintaxis #(...)
. En el cuerpo de la función anónima podemos referenciar el argumento de la función usando %
. Si la función tuviera más de un argumento, podríamos referenciarlos usando %1
, %2
y así sucesivamente.
El resultado de aplicar la función parse-file
a un fichero es una lista de recursos en el que cada elemento es un vector que contiene el identificador de recurso como primer elemento y el texto como segundo elemento:
(["1" "Aceptar"] ["2" "Cancelar"] ["3" "Eliminar"])
Ahora que tenemos declarada la función parse-file
podemos empezar a usarla:
(def es-orig (parse-file "app1.es.txt")) (def es-new (parse-file "app2.es.txt")) (def fr-orig (parse-file "app1.fr.txt"))
Con este código definimos tres variables con el resultado de parsear los ficheros de recursos correspondientes a las distintas aplicaciones e idiomas.
Cómo probarlo
La forma más sencilla de ejecutarlo es a través del REPL de leiningen que veíamos en la introducción a clojure. Podemos guardar el programa en un fichero y cargarlo en el REPL de leiningen usando la función load-file
:
user=> (load-file "merge.clj") user=> (ns com.koalite) com.koalite=>(pprint es-orig) (["1" "Aceptar"] ["2" "Cancelar"] ["3" "Eliminar"])
Al cargar el fichero se definirán las funciones y variables contenidas en el mismo. Puesto que todo lo hemos definido en el espacio de nombres com.koalite
, para poder usarlas será necesario indicar que queremos trabajar con ese espacio de nombres usando la forma especial (ns com.koalite)
.
A partir de ese momento, podemos comprobar el valor de las variables y ejecutar operaciones sobre ellas aprovechando el REPL (algo impagable a la hora de depurar sin depurador).
Resumen
Lo que hemos visto hasta ahora nos sirve para ver algunas cosas frecuentes en clojure, como el uso de funciones anónimas, la definición de variables o la clara orientación funcional del lenguaje.
De momento sólo hemos parseado los ficheros para poder procesarlos. En el próximo post veremos como obtener los recursos que se mantienen entre las dos aplicaciones para generar el fichero de salida, consiguiendo así nuestro objetivo.