Condicionales, bucles e iteración
Un programa es un proceso que ejecuta la máquina con la instrucción que toca en ese momento ─obviando el procesamiento paralelo que puede ejecutar varias cosas a la vez─. Si todo, el 100% del código, se ejecutara a la vez, el programa no funcionaría y seguramente la máquina no sobreviviría a tamaño calentón. Eso ocurre también con nuestro cerebro, que está activado sobre el 10% o 15% y hay quien piensa que sólo usamos eso de él. En realidad lo usamos todo, el 100%, pero sólo cuando toca. También existen personas que tienen episodios de una activación mayor y pueden llegar hasta el 40%. A esa activación extraordinaria la llamamos epilepsia y los síntomas son los propios de activaciones de conjuntos neuronales funcionando cuando no toca.
Si nuestro programa ejecuta una instrucción tras otra, siempre en el mismo orden y siempre de la misma forma, nuestro programa será tan rígido que seguramente será poco útil. Necesitamos que nuestro código sea más flexible y según las condiciones que se den, realice unas acciones u otras, adaptándose a las diferentes circunstancias. Hoy toca hablar de esos pichorros que nos permiten hacer que nuestro programa haga cosas distintas tomando decisiones según las circunstancias, o las hagan varias veces.
Variables globales y locales
Antes de meternos con el tema del control de ejecución hay que hacer
una puntualización sobre la definición de variables. Hasta ahora hemos
utilizado en nuestros ejemplos el comando set
, y su primo-hermano
monocigótico setq
, para definir y establecer las variables y sus
valores.
Las variables ocupan espacio y hay ocasiones en las que necesitamos guardar un determinado valor sólo durante los momentos en que se realizan los cálculos, por lo que gastar el espacio que ocupa una variable durante toda la sesión cuando sólo la hemos necesitado unos pocos segundos es un derroche de medios que debemos evitar.
En otras ocasiones, efectivamente, necesitaremos valores durante más tiempo y eso merece que le hagamos un hueco de forma más permanente a esa variable. ¿Cuándo debemos emplear una u otra? Pues nos lo dirá el sentido común y la necesidad.
Globales
Para definir variables globales en nuestro código es recomendable
utilizar la instrucción defvar
, con el siguiente formato:
(defvar mi-variable 4 "Esta variable se utiliza para almacenar un entero.")
En realidad, lo único necesario sería (defvar mi-variable)
. Eso solo
ya reserva el espacio y nuestro programa ─y Emacs en general─, la
reconocerá como válida. Sin embargo yo he utilizado la definición
completa. ¿Por qué? La respuesta es obvia se después de evaluar el
código anterior evaluamos (apropos "mi-variable")
. ¿Lo
ves?... ¡efectivamente! apropos
nos mostrará la cadena de
documentación que hayamos escrito.
Puedes seguir utilizando set
, nada te lo impide, pero al escribir
código será mucho más evidente que has querido crear una variable
global ─si además documentas para qué la usas es ya de premio─ si
utilizas la forma defvar
.
También existe una forma defconst
, que como su nombre indica implica
el deseo de definir una constante, ─es decir, un valor que se
mantendrá fijo durante la ejecución de nuestro programa─. Pero tiene
un problema estructural básico: Sólo es un deseo, no es
efectivo... sólo hay que comprobarlo con el siguiente código:
(defconst MI-CONSTANTE 3 "Una constante cualquiera que quiero definir.") ; ==> mi-constante = 3 (set 'MI-CONSTANTE 4) ; ==> mi-constante = 4
No hay ningún aviso de error ni nada que bloquee el que cambiemos el
valor de una constante, lo cual no parece tener mucho sentido. En
Emacs los símbolos mi-constante
y MI-CONSTANTE
son distintos. Yo
suelo utilizar las constantes en mayúsculas para ver a simple vista
que estoy intentando modificar algo que pensé como constante en el
código y no se me crucen las cosas. También suelo utilizar defconst
aún siendo de poca utilidad, porque cuando leo el código estoy
seguro que quise definir algo como constante y mis motivos tenía ─los
suelo escribir en la cadena de documentación de la definición─.
Variables locales
Para las ocasiones en las que sólo necesitamos las variables durante
el tiempo que evaluamos el código y no lo necesitaremos fuera de él,
podemos utilizar las formas let
y let*
, que como set
y setq
son primas-hermanas monocigóticas, para declarar variables. Tanto
let
como let*
tienen una estructura similar:
(let (variables) (cuerpo))
Quizá con un ejemplo se vea más claro:
(let ((x 3) (y 5)) ; aquí se cierra el bloque de «variables» (+ x y)) ; aquí está el bloque «cuerpo» y se cierra la forma let
Si evaluamos el código anterior el resultado será 8. Vale, ¿y qué lo
diferencia de let*
? Vemos el siguiente ejemplo:
(let* ((x 3)
(y (+ x 2)))
(+ x y))
Prueba el código con el *
y sin él. ¿Qué ocurre cuando no está? ¿Un
error? ¿Qué error? ... algo así como «la variable x no existe». ¿Por
qué con el asterisco sí existe? Pues básicamente, porque Lisp va
evaluando el código de la definición de las variables según se las va
encontrando, así, ha podido definir y
en base al valor de x
. En el
caso de no utilizar asterisco, simplemente x
no está disponible
hasta que no se cierre el bloque de las variables.
Si evaluamos el símbolo x
o y
fuera de la forma let
, elisp nos
dirá que esa variable no existe, porque sólo existen dentro de la
forma let
.
Formas
Lo he traducido como formas, en Lisp se llama form
a todo lo que
se mete entre paréntesis con el fin de ser evaluado. Lo bueno de estas
formas es que se pueden meter unas dentro de otras, como se puede
intuir de los ejemplos anteriores. Es muy habitual que veamos cosas
como por ejemplo:
(defun simbolo-funcion ... (let (variables) (cuerpo)))
Se puede ver cómo se van acumulando paréntesis en algunos sitios. Al principio te preocupa tanto paréntesis junto, temes olvidar alguno, sin embargo, con la práctica verás que es fácil de entender: sólo son formas dentro de formas que están dentro de formas y tu pensamiento se centrará en esas formas y no en los paréntesis.
Condicionales
If
La forma if
es la más sencilla de todos los condicionales. Su
estructura es muy fácil:
(if (condición)
(bloque evaluado si es «t»)
(bloque evaluado si es nil))
Vemos un ejemplo para ver cómo funciona:
(if (y-or-n-p "¿Tú que dices?") (message "¡¡¡Has dicho que síiii!!!") (message-box "Pues no, pues vale."))
He utilizado dos formas interactivas para mostrar un mensaje dentro
de una if
. La forma y-or-no-p
muestra un mensaje en el
minibuffer de emacs, el que le pasamos como cadena de caracteres,
y espera a que el usuario pulse y
si está de acuerdo o n
si no lo
está. Devolverá t
en el primer caso y nil
en el segundo. La forma
message
simplemente muestra un mensaje en el minibuffer. La forma
message-box
muestra el mensaje en un diálogo de ventana con su botón
y todo.
When y unless
Podríamos decir que when
y unless
son formas resumidas de if
. La
primera, when
equivale a utilizar sólo el bloque cierto de la
expresión. Dicho de otro modo, sólo se ejecutará el bloque de código
si la condición es cierta. La forma unless
, al contrario, sólo
ejecutará el código si resulta la condición falsa.
Podríamos escribir when
como:
(when (condición) (bloque de código)) ; será completamente equivalente a (if (condición) (bloque de código) nil)
Y también podríamos escribir unless
como
(unless (condición) (bloque de código)) ; será completamente equivalente a (if (condición) nil (bloque de código))
Cond
Hemos visto que if
evalúa sólo una condición para actuar en
consecuencia. La forma cond
lo que hace es evaluar una condición
tras otra mientras y ejecuta sólo el cuerpo de la primera condición
que se cumpla.
A ver, más despacio para no perdernos:
(cond (lista de condiciones))
donde cada condición tiene la forma:
(condición forma-código)
Vamos a poner un ejemplo a ver cómo funciona:
(cond ((numberp x) (message "x es un número")) ((stringp x) (message "x es una cadena")) (t (message "x no es ni una cadena ni un número")))
Y para probarlo podemos ir evaluándolo con los siguientes valores:
(set 'x 4)
(set 'x "Hola")
(set 'x nil)
Bien, también aprovecho para contar que la tercera forma dentro de la
lista de condiciones de cond
que he puesto en el ejemplo, se
evaluará siempre si ninguna de las anteriores es evaluada como cierta.
Si no se pone, la forma cond
devolverá un nil
, que a veces no
será un valor esperado.
Condiciones compuestas
Hay ocasiones en que las condiciones pueden ser más complejas que un
sí o un no, sino una combinación de valores o cláusulas
booleanas. Las funciones booleanas son and
, or
y not
.
(not t) ; ==> nil (not nil) ; ==> t (and t t) ; ==> t (and t nil) ; ==> nil (and nil t) ; ==> nil (and nil nil) ; ==> nil (or t t) ; ==> t (or t nil) ; ==> t (or nil t) ; ==> t (or nil nil) ; ==> nil
Vamos a evaluar un par de expresiones para ver cómo funcionan:
(and (message-box "Hola 1") (message-box "Hola 2") nil (message-box "Hola 3")) (or (message-box "Hola 1") (message-box "Hola 2") nil (message-box "Hola 3"))
Si evaluamos la expresión and
veremos que nos aparecen dos
message-box
y se detiene en tercer elemento, devolviendo nil
. Sin
embargo, en la expresión or
sólo muestra el primero y devuelve la
cadena "Hola 1"
evaluando la expresión, por tanto como verdadera.
Bucles
Los bucles sirven para ejecutar código de forma repetitiva. El bucle
más habitual es while
. Pongo un ejemplo y lo destripamos:
(let ((num 0)) (while (< num 4) (princ (format "Iteración %d.\n" num)) (setq num (1+ num))))
Veremos que al evaluar el código escribe lo siguiente:
Iteración 0. Iteración 1. Iteración 2. Iteración 3. nil
En el ejemplo el bloque de código es muy simple, sólo utiliza la forma
princ
para escribir una cadena de formato para mostrarnos la
vuelta en la que está. En general, es habitual encontrar una
estructura del while
de la siguiente manera:
(... establecer-contador
(while (condición contador)
(bloque-de-código)
(actualizar-contador)))
En otros lenguajes podemos un bucle adicional, que primero ejecuta el
código y luego evalúa la condición. A esos bucles se les suele llamar
while-until
y aunque en Lisp no hay una forma equivalente con
nombre podemos simularla poniendo una forma progn
justo dentro de la
cláusula condición del while
de esta manera:
(let ((num 0)) (while (progn (princ (format "Iteración %d.\n" num)) (set 'num (1+ num)) (y-or-n-p "¿Repetir otra vez?"))))
Si lo evaluamos mostrará el mensaje mientras contestemos y
a la
pregunta de si repetimos otra vez.
Hay otras formas de iteración como dolist
que repite el proceso a lo
largo de los elementos de una lista o dotimes
que lo repite el
número de veces que se establezca. Los dejaré para más adelante cuando
las necesitemos en algún ejemplo.
Conclusión
He dejado para otro artículo una forma de construir bucles más compleja, que se llama iteración. Son funciones que se llaman a sí mismas para hacer un determinado proceso. Pero como lo veremos en otra entrega no diré más aquí.
Comentarios