Breve excursión por Lisp


Introducción

Esta página hace un rápido recorrido por algunos aspectos de Lisp, sin profundizar en ninguno de ellos. Se tratan aspectos que son comunes en prácticamente todos los lenguajes de programación. Eso sí, con el estilo particular que imprime este lenguaje a su código.

Para ejecutar una función de nombre fun que admite los argumentos s1, ..., sn, en Lisp debemos escribir así:

(fun s1 ... sn)

En otros lenguajes haríamos de esta otra manera:

fun (s1, ..., sn)

Números

En Lisp podemos utilizar números en forma entera, decimal, fraccionaria o compleja. Las operaciones aritméticas básicas son la suma (+), la resta (-), el producto (*) y la división (/).

Empezamos por una simple suma,

(+ 3 4)
7

Para calcular la expresión numérica \(5 + 1.7 \cdot 6.8\) necesitamos anidar dos llamadas a funciones,

(+ 5 (* 1.7 6.8))
16.560001

Si hacemos esta cuenta manualmente, el resultado que obtenemos es 16.56 y uno puede pensar que Lisp ha cometido un error. No es así; todo sistema digital debe traducir los números decimales a su forma binaria antes de operar con ellos, y es el caso que números que son decimales exactos en el sistema decimal son periódicos en el binario, y como los resultados deben redondearse, ahí es donde se produce el error.

También podemos hacer operaciones con números fraccionarios, calculamos \(5 - \frac{8}{9}\),

(- 5 8/9)
37/9

Si en las operaciones aparece un número decimal, se produce un efecto de contagio de manera que el resultado devuelto también es decimal; así obtenemos \(5.2 - \frac{8}{9}\),

(- 5.2 8/9)
4.311111

Calculamos el cociente de dos números complejos, \[ \frac{3-4 i}{7-3 i} \]

(/ #c(3 -4) #c(7 -3))
#C(33/58 -19/58)

Así, el resultado es \(\frac{33}{58} - \frac{19}{58} i\).

Vemos que para representar el número complejo \(a + b i\) debemos utilizar el formato #c(a b). También vemos que si en los números introducidos no hay decimales, el resultado que se nos ofrece tiene sus partes real e imaginaria en forma de fracción.

Para calcular una raíz cuadrada usamos la función sqrt. Calculamos \(\sqrt{5}\),

(sqrt 5)
2.236068

Como siempre en Lisp, dentro de la lista se escribe en primer lugar el nombre de la función y a continuación sus argumentos.

Para el cálculo de potencias utilizamos la función expt, que tiene dos argumentos, la base y el exponente. Calculamos \(17^{25}\),

(expt 17 25)
5770627412348402378939569991057

Variables

Las variables permiten almacenar en ellas cualquier tipo de información. Con la función defvar podemos definir una variable al tiempo que le asignamos un valor,

(defvar x 3)
X

Responde con el nombre de la variable en mayúscula. Comprobamos su valor sin más que escribir su nombre en la línea de edición,

x
3

Ahora podemos hacer uso de la variable multiplicándola por cinco,

(* 5 x)
15

Si queremos cambiar su valor, utilizamos la función setf,

(setf x 7)
7

Comprobamos el cambio,

x
7

La variable anterior es global y accesible desde cualquier sitio dentro de nuestra sesión. A veces interesa que la variable tenga una existencia más efímera, que exista solo durante el tiempo que es necesaria, como por ejemplo para almacenar un dato de forma temporal. Una de las maneras de definir variables locales es mediante la función let. Aquí la utlizamos para almacenar el valor del radio de un círculo y calcular su área,

(let (r)
  (setf r 5)  ; asignamos el valor 5 a r
  (* pi r r)) ; área del círculo
78.53981633974483d0

El primer argumento de let es una lista de variables locales y a continuación la serie de instrucciones a ejecutar; la función devuelve el resultado de la última instrucción. En este ejemplo, el resultado es devuelto en notación científica de doble precisión; de ahí la letra d precediendo al último cero, que es el exponente de diez.

Este último ejemplo introduce los comentarios, los cuales van precedidos del punto y coma. Todo el texto que se escriba tras el punto y coma y hasta el final de la línea de texto, es ignorado.

También es posible con let asignarle un valor a la variable local en el momento de declararla,

(let ((r 5))
  (* pi r r))
78.53981633974483d0

En cualquier caso, una vez let ha terminado su ejecución y devuelto el resultado, si solicitamos el valor de la variable local, se detectará un error y entraremos en el modo depuración,

r
debugger invoked on a UNBOUND-VARIABLE in thread
#:
  The variable R is unbound.

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [ABORT] Exit debugger, returning to top level.

(SB-INT:SIMPLE-EVAL-IN-LEXENV R #)
0] 

Lisp nos dice que The variable R is unbound. Escribimos 0 para salir del depurador.

Un ejemplo con varias variables locales, unas con asignación y otras no. Calculamos el capital final de un plan de ahorro a 3 años y medio al 2% anual, invirtiendo una cuota mensual de 200€; la fórmula a aplicar es \[ C_f = C \left( 1 + \frac{r}{p 100}\right) \frac{\left( 1 + \frac{r}{p 100}\right)^n - 1}{\frac{r}{p 100}}, \]

(let ((c 200) ; a c, r y p se les asignan valores numéricos
      (r 3)
      (p 12)
      n       ; n y s se declaran pero sin asignaciones
      s)
  ; aquí se asignan valores a n y s
  (setf n (* p 3.5)
        s (/ r (* p 100)))
  ; empieza el cálculo que es devuelto por let
  (* c
     (+ 1 s)
     (/ (- (expt (+ 1 s) n) 1)
        s)))
8867.318

En el código anterior, hemos aprovechado la posibilidad que nos brinda setf para hacer múltiples asignaciones a más de una variable. También vemos la importancia de una correcta indentación del código para hacer la lectura fácil. Las fórmulas matemáticas no son fáciles de interpretar en Lisp a primera vista y requieren de cierto entrenamiento.

Como antes, una vez terminada la ejecución de let, las variables locales son borradas del sistema.

Listas

Las listas son la esencia de Lisp, cuyo nombre ya lo reconoce, ya que proviene de List Processing, procesamiento de listas.

Las listas se escriben entre paréntesis y sus elementos se separan por espacios. Los programas también son listas y sus sentencias también se separan con espacios. Lisp siempre da por hecho que las listas son programas e intentará ejecutarlos; para indicarle que la lista no se quiere ejecutar escribimos una comilla sencilla justo antes del primer paréntesis,

'(2 4 6 8 10)
(2 4 6 8 10)

Si hubiésemos escrito la lista de pares sin la comilla simple, Lisp intentaría ejecutar la primera instrucción, y como 2 no es el nombre de ninguna orden conocida, respondería con un error entrando en modo depuración.

Guardamos los nombres de las provinvias gallegas en una variable; las cadenas de texto se escriben con comillas dobles,

(defvar gal '("A Coruña" "Lugo" "Ourense" "Pontevedra"))
GAL

No tengo pruebas objetivas de ello, pero no me extrañaría que las funciones Lisp que más se utilizan con listas sean car y cdr. La primera extrae el primer elemento de la lista,

(car gal)
"A Coruña"

La segunda devuelve lo que queda de la lista cuando se ignora el primer elemento,

(cdr gal)
("Lugo" "Ourense" "Pontevedra")

Podemos jugar con las letras centrales de estas dos funciones para extraer otro tipo de información; por ejemplo, para extraer el segundo elemento de la lista, es tanto como aplicar un car después de un cdr,

(car (cdr gal))
"Lugo"

Pero Lisp nos permite hacerlo de forma más compacta,

(cadr gal)
"Lugo"

La lista de las dos últimas provincias sería tanto como aplicar dos veces cdr,

(cddr gal)
("Ourense" "Pontevedra")

También es posible acceder a un elemento de la lista con el ordinal en inglés,

(fourth gal)
"Pontevedra"

Para listas grandes, la función a utilizar para obtener elementos de la lista es nth,

(nth 3 gal)
"Pontevedra"

En este caso, hay que tener en cuenta que los elementos de la lista se enumeran a partir de cero.

Para saber el número de provincias de Galicia,

(length gal)
4

Hay quien dice que Buenos Aires es la quinta provincia gallega. Pues se la añadimos,

(cons "Buenos Aires" gal)
("Buenos Aires" "A Coruña" "Lugo" "Ourense" "Pontevedra")

No obstante todo lo que hemos hecho con la lista gal, ésta ha permanecido invariable,

gal
("A Coruña" "Lugo" "Ourense" "Pontevedra")

Si queremos modificar la lista para que incluya la capital criolla usaremos la función setf que ya conocemos,

(setf gal (cons "Buenos Aires" gal))
("Buenos Aires" "A Coruña" "Lugo" "Ourense" "Pontevedra")

Para ser consecuentes con la legislación gallega vigente, la toponimia local debe escribirse en lengua gallega, pues a ello,

(setf (nth 0 gal) "Bos Aires")
"Bos Aires"

Comprobamos el cambio,

gal
("Bos Aires" "A Coruña" "Lugo" "Ourense" "Pontevedra")

No abandonamos esta sección sobre las listas sin dejar de indicar que pueden contener cualquier tipo de objetos, incluso otras listas; esta posibilidad las hace muy versátiles, ya que con ellas se pueden almacenar todo tipo de estructuras de datos.

El siguiente árbol clasifica parcialmente al reino animal:

Esta estructura en árbol se puede codificar en Lisp de la siguiente manera,

'(("Invertebrados"
    "Poríperos"
    "Celentéreos"
    ("Gusanos" "Anélidos" "Platelmintos" "Nematelmintos")
    "Moluscos"
    "Equinodermos"
    ("Artrópodos" "Insectos" "Arácnidos" "Crustáceos" "Miriápodos"))
  ("Vertebrados"
    ("Peces" "Óseos" "Cartilaginosos")
    "Anfibios"
    "Reptiles"
    "Aves"
    ("Mamíferos" "Carnívoros" "Hervíboros" "Voladores" "Acuáticos" "Primates")))

Lisp dispone de recursos más que suficientes para gestionar estas estructuras.

Funciones

Lisp se inscribe dentro del grupo de lenguajes funcionales y escribir funciones en Lisp es la tarea en la que se ve envuelto el programador de forma constante. Vamos a escribir una función que calcule el volumen de un cilindro en función del radio y la altura,

(defun vol (r a) (* pi r r a))
VOL

Debemos utilizar la palabra defun seguida del nombre que le queramos dar a la función, le sigue la lista de los argumentos que se le deben pasar y la sentencia o sentencias que se deben ejecutar. Una vez definida la función, podemos hacer uso de ella cuanto queramos para calcular volúmenes de cilindros,

(vol 5 7)
549.7787143782139d0

Si una función no necesitase argumentos, la lista de los mismos debe estar vacía,

(defun saluda () "hola")
SALUDA

Y saludamos ...

(saluda)
"hola"

Condicionales

Todo lenguaje de programación debe disponer de condicionales que permitan tomar decisiones sobre qué acciones ejecutar en función de que se cumplan o no ciertas condiciones.

Más arriba escribimos una función para calcular volúmenes de cilindros. Estaría bien introducir un condicional que nos permitiese detectar si alguno de los argumentos, radio o altura, fuese negativo, y en tal caso emitir un mensaje de error,

(defun vol2 (r a)
  (if (or (<= r 0) (<= a 0))
    (print "Argumento incorrecto"))
  (* pi r r a))

La sentencia if comprueba si alguno de los argumentos tiene asignado un valor no positivo. Tiene dos argumentos, el primero es la condición y el segundo lo que debe hacer si la condición se cumple, en este caso, imprimir un mensaje.

(defun vol2 (r a)
  (if (or (<= r 0) (<= a 0))
    (print "Argumento incorrecto"))
  (* pi r r a))
VOL2

Y lo probamos,

(vol2 2 -3)
"Argumento incorrecto" 
-37.69911184307752d0

Vemos que imprimió el mensaje y aún así hizo el cálculo. Hacemos un pequeña modificación del código,

(defun vol3 (r a)
  (if (and (> r 0) (> a 0))
    (* pi r r a)
    "Argumento incorrecto"))
VOL3

Y probamos otra vez,

(vol3 2 -3)
"Argumento incorrecto"
(vol3 2 3)
37.69911184307752d0

La función que hemos definido contiene una única sentencia if y admite dos o tres argumentos, siendo el último opcional. El primer argumento es la condición sobre la cual se toma la decisión, en este caso se comprueba si los dos argumentos toman valores positivos, el segundo es lo que debe ejecutar si la condición se cumple y el tercero, el opcional, lo que debe hacer si no se cumple. Con el tercer argumento hacemos lo que en otros lenguajes se conoce como una sentencia if-then-else.

En la definición de vol3 hemos cambiado la condición para ver el uso de los dos operadores lógicos and y or.

Otros condicionales de uso frecuente son when, a utilizar cuando queremos ejecutar más de una sentencia en caso de cumplirse la condición, y cond, para múltiples comparaciones. Vemos un ejemplo de este último para definir la función a trozos \[ f(x)= \left\{ \begin{array}{cl} x^2 & \mbox{ si } \lt 1 \\ x+3 & \mbox{ si } 1 \leq x \lt 2 \\ 5 & \mbox{ otro caso } \end{array} \right. \]

(defun f (x)
  (cond
    ((< x 1)
       (* x x))
    ((< x 2)
       (+ x 3))
    (t
       5)))
F

Los argumentos de cond son listas, el primer elemento de cada una de ellas es la condición que se debe cumplir para que se ejecute el resto de esa lista; ejecutada ésta, ya no se hacen más comprobaciones y se termina el condicional. La condición del último par suele ser el símbolo t, que representa el valor lógico VERDADERO, que se cumple siempre si han fallado todas las condiciones anteriores. Calculamos \(f \left(\frac{2}{5} \right)\),

(f 2/5)
4/25

Resta decir que el valor lógico FALSO es el símbolo nil.

Iteraciones

Al igual que los condicionales, las posibilidad de iterar y repetir unas mismas instrucciones una y otra vez de forma rutinaria es otra de las posibilidades comunes en todos los lenguajes de programación.

Hay muchas maneras de hacer iteraciones o bucles en Lisp, una de ellas es loop; se ejecutan una y otra vez una serie de sentencias hasta que se cumpla una condición controlada por un condicional y el control ejecute una sentencia return, la cual hará que se termine la iteración devolviendo un resultado. La siguiente función calcula el factorial de un número, \(n! = 1 \cdot 2 \cdot \ldots \cdot n\),

(defun factorial (n)
  (let ((f 1)
        (c 1))
    (loop
      (setf f (* f c))
      (setf c (+ c 1))
      (if (> c n)
        (return f)) )))
FACTORIAL

Mediante let declaramos dos variables locales, f para ir guardando los sucesivos productos, y c para actuar de contador de las multiplicaciones que vamos realizando. A continuación hay una instrucción loop, la cual contiene tres sentencias, multiplica f por el contador actual del bucle, actualiza el contador y luego controla si el contador excede del número cuyo factorial queremos calcular; si la condición se cumple, se sale del bucle con el valor que guarda la variable f; como el bucle es la última instrucción del programa, el valor de f es también la respuesta de la función.

Calculamos \(30!\),

(factorial 30)
265252859812191058636308480000000

Otra instrucción de Lisp para ejecutar iteraciones es dolist, con ella aplicamos un mismo procedimiento a todos los elementos de una lista. La siguiente función escribe los signos de una lista de números haciendo uso de la instrucción cond vista más arriba,

(defun signos (lista)
  (dolist (e lista)
    (cond
      ((> e 0)
         (print "positivo"))
      ((< e 0)
         (print "negativo"))
      (t
         (print "nulo")))) )
SIGNOS

El primer argumento de dolist es una lista que contiene el nombre de la variable local e y la lista que contiene los valores que esta variable va a ir tomando en cada iteración; dentro del cuerpo de dolist hay una sola instrucción, que es un cond que decidirá los signos de cada valor que va tomando e. No es necesario declarar con un let la variable e, ya que ésta es local a dolist. La instrucción dolist devuelve el valor lógico NIL.

Probamos la función con una lista de números,

(signos '(-5 6 8/9 -5/8 0 3.5 -8.7))
"negativo" 
"positivo" 
"positivo" 
"negativo" 
"nulo" 
"positivo" 
"negativo" 
NIL

Ni mucho menos estas dos instrucciones completan todas las posibilidades de Lisp en lo que a iteraciones se refiere.


© 2016, TecnoStats