Notxor tiene un blog

Defenestrando la vida


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 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

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en Mastodon y en Diaspora con el nick de Notxor.

También se ha abierto un grupo en Telegram, para usuarios de esa red.

Disculpen las molestias.