lunes, 15 de febrero de 2010

Ejemplo básico Clojure I: multiplicar números como listas.

(Si queréis poner en practica el ejemplo de esta serie de posts aquí tenéis unas instrucciones precisas de como instalar y los primeros pasos con Clojure .)

Hace poco me examine de la asignatura de Programación Declarativa en la UNED que consiste en gran parte en programar en Haskell. Tal vez algun día me ocupe de este lenguaje, funcional como Clojure pero con una sintaxis mas barroca y todo un universo propio en el que se intenta mantener la pureza funcional y la referencia transparencial a base encerrar lo impuro en unas carceles llamadas monadas. Un lenguaje fascinante pero tal vez un paso mas alejado del mundo de los negocios que otros lenguajes funcionales modernos como clojure, scala o erlang.
Uno de los ejercicios que se proponían para practicar era el siguiente, extraido del manual que seguíamos en la asignatura: dado dos números representados como una lista de sus dígitos, declarar una función que devuelva en el mismo formato el resultado de multiplicarlos, así como las funciones para convertir un numero en una lista en ese formato y otra que haga la operación inversa. La ventaja de representar así los números es que puedes sobrepasar el límite establecido para los tipos numéricos enteros habituales.

Ejemplo de lo que queremos:

->(def x (numList 12345))
(1 2 3 4 5)

->(def y (numList 12))
(1 2)

->(mult x y)
(1 4 8 1 4 0)

->(listNum (mult x y))
148140

En esta primera entrada nos pondremos con las función auxiliar que nos convierten un número en una lista. Para conseguir nuestra lista de dígitos habrá que dividir el número por 10, acumular el resto en la lista y volver a realizar la operación con el cociente de la división anterior hasta que la división nos de 0:

12345/10 -> cociente:1234 restos:(5)
1234/10-> 123 (4 5)
123/10-> 12 (3 4 5)
12/10-> 1 (2 3 4 5)
1/10-> 0 (1 2 3 4 5)

La descripción de la función ya nos da la pista de que usar la recursión será la solución mas elegante y ajustada. Como vamos a usar el resto y el cociente de la división entera nos haremos una función auxiliar que nos devuelva ambos resultados en un vector:

(defn div [x y] [(quot x y) (rem x y)])

-> (div 5 3)
[1 2]

La primera aproximación usando la recursión tal cual será hacernos una función auxiliar iNumList que irá haciendo las divisiones y acumulando los restos y que nos devolverá la lista acumulada cuando la división sea igual a 0, y llamar a esa función con una lista vacia inicial desde la función principal:

(defn numList [num] (iNumList () num))

(defn iNumList [acc x]
(let [[cociente resto] (div x 10) lista (cons resto acc)]
(if (zero? cociente) lista (iNumList lista cociente))))

Veamos la función iNumList. En primer lugar se usa let. Habria mucho que decir de esta forma especial que forma parte del nucleo mismo de Clojure pero basicamente enlaza símbolos con expresiones o valores constantes dentro de un vector y los pone a disposición de las expresiones que siguen a ese vector. En cada uno de los enlaces estan también disponibles los símbolos anteriores. Let soporta el enlace basado en la estructura abstracta (o "destructuracion") de la expresion a enlazar. Consiste en que podemos usar la estructura del dato para realizar el enlace. Se ve mejor con un ejemplo:

-> (let [[x,y] [10,2] z (* x y)] (list x 'por y 'igual 'a z))
(10 por 2 igual a 20)

Gracias a la destructuración del vector [10,2] usando su misma estructura en los simbolos [x,y] nos hemos ahorrado el tener que hacer:

->(let [v [1,2] x (v 0) y (v 1) z (* x y)] (list x 'por y 'igual 'a z))
(10 por 2 igual a 20)

En java no existe la destructuracion y la traduccion al mismo del ultimo codigo mas o menos seria:

Object[] ejemploLet () {
int[] v=new int[] {10,2};
int x=v[0],y=v[1],z=x*y;
return new Object[] {new Integer (x),"por",new Integer (y),
"es",new Integer (z)};
}

Volviendo a la función vemos que gracias a let tenemos nuestro resto, cociente y la lista en la que estamos acumulando los restos. Para ir construyendo la lista se usa cons que inserta un elemento en la cabeza de una lista. Usando if hacemos la recursión en el caso de que no sea 0 el cociente llamando a la misma función con el nuevo cociente.

Sin embargo el uso de la recursión tiene un pequeño problema ya que la parte de la memoria que se usa para almacenar los valores de cada llamada (stack) es mas limitada que la memoria normal donde se almacenan los datos de un programa (heap). Para numeros muy, muy grandes la llamada provocaría el famoso java.lang.StackOverflowError. Para evitarlo en Clojure usa una forma especial de hacer la recursión optimizada , consiguiendo la efectividad de los lenguajes imperativos sin perder la elegancia de la recursión. Esta forma especial se consigue con recur y tiene una importante limitación: debe estar en la última expresión evaluada de la función. Asociandolo a otra forma especial, loop podemos hacer una recursión segura y evitarnos la función auxiliar incrustandola dentro de la principal:

(defn numList [x]
(loop [acc () sig x]
(let [[cociente resto] (div sig 10) lista (cons resto acc)]
(if (zero? cociente) lista (recur lista cociente)))))

Loop marca el punto sobre el que se hace la recursión y enlaza en cada nueva llamada los parametros en un let implicito, iniciandolo con los valores () y el número original en la primera llamada.

En siguientes posts seguiremos desarrollando este ejemplo introductorio.

No hay comentarios:

Publicar un comentario