Programar en Forth con el estilo de Forth

Descripción del contenido de la página

Ejemplos de código escrito en Forth sin el estilo de Forth, y cómo reescribirlos.

Etiquetas:

Estoy trabajando en la adaptación de varios programas antiguos en Forth a sistemas Forth más modernos. En el proceso me estoy encontrando algunas muestras de código que delatan que el código está calcado de una versión anterior en BASIC o en otro lenguaje; o bien que el programador aún no escribía código Forth auténtico sino que seguía pensando con la misma estructura mental a la que obligan los lenguajes más conocidos y después transcribía el resultado a palabras de Forth.

Recientemente escribí un artículo titulado Programar en PHP con el estilo de Forth. Creo que es interesante completarlo con este, para mostrar ejemplos de código en Forth que no está escrito siguiendo los principios y el estilo de Forth, y cómo reescribirlo.

Ejemplo 1

En el programa Sideras, escrito en Ace Forth, encontré la siguiente palabra:

: izq
  x @ 0 > ascii 5 k= and
  if
    x c@ 1- x !
  then
;

La palabra k= a su vez está definida previamente así (pero no es relevante para el ejemplo):

: k=  inkey =  ;

Como es fácil deducir por el código y por el nombre del programa, Sideras es un juego. La palabra izq comprueba si la tecla 5 está pulsada y si la coordenada x del jugador aún es mayor que cero; entonces la decrementa. La traducción literal es:

Si el contenido de x es mayor que cero y la tecla 5 está pulsada, entonces toma de nuevo el contenido de x, réstale uno y guárdalo en x.

Claro que funciona... pero eso no es el estilo de Forth. ¿Por qué? En lugar de intentar explicar cuál es el estilo de Forth, veámoslo sin más:

: izq  x dup @ ascii 5 k= - 0 max swap !  ;

No pretendo decir que el código anterior sea el mejor posible; es lo primero que me vino a la cabeza cuando me propuse reescribir la palabra izq con una codificación en Forth más genuina. En Forth hay tantas maneras de hacer cada cosa como programadores.

Lo primero que llama la atención al comparar mi propuesta con el original es que mi código, para quien no está acostumbrado a trabajar con Forth, parece un galimatías, mientras que el original parecía tener cierta estructura. Pero un programador de Forth sentirá que mi código está mucho más cerca de la poesía interna del lenguaje y lo entenderá fácilmente. La traducción literal ahora es:

Toma la dirección de la variable x y haz un duplicado. Toma su contenido. Réstale el resultado de comprobar si la tecla 5 está pulsada. Quédate con lo que sea mayor: el cálculo anterior o 0. Coloca el duplicado de la dirección en su lugar y guarda en ella el resultado final.

Como se ve, explicar lo que hace el código necesita ahora más palabras que antes (por suerte no es así como tenemos que contárselo a la computadora). Pero el nuevo código es más sencillo; y me atrevería a añadir que más natural, si tal palabra tuviera su lugar en el cosmos de la programación. Veamos:

Mostrado de otra manera:

código original código reescrito
palabras 15 11
estructuras de control 1 0
cálculos 2 1
menciones a la variable x 3 1

Hay un detalle importante que tener en cuenta para entender cómo funciona el código: Ace Forth es una versión de fig-Forth. En fig-Forth el resultado de una operación lógica devuelve 1 o 0. Ese será el resultado de la operación ascii 5 k=, que por tanto podemos restar sin más al valor de la variable x. En un Forth más moderno, como ANS Forth, el resultado de las operaciones lógicas es -1 o 0 (lo cual es más lógico y más útil, porque -1, en binario, tiene todos sus bitios activos, lo que simplifica muchas operaciones) y la palabra debería escribirse con un sencillo cambio: utilizar + en lugar de -:

: izq  x dup @ [char] 5 k= + 0 max swap !  ;

El cambio de ascii, palabra de fig-Forth, a [char], la palabra equivalente en ANS Forth, no es relevante para el ejemplo.

Ejemplo 2

Veamos un ejemplo más completo. Tomo como partida el código de un micro juego para la Jupiter Ace llamado Chase. El listado de Chase fue publicado por Andrew Curtis en un número de comienzos de 1983 de la revista semanal Popular Computing Weekly.

Código original

El código original, escrito de corrido, es prácticamente incomprensible:

10 VARIABLE X
15 VARIABLE Y
1 VARIABLE A
1 VARIABLE B

: CHASE CLS 10 X ! 15 Y ! 1 A ! 1 B ! BEGIN X @ Y @ AT ."  " A @ B @ AT ."  " INKEY 53 = IF Y @ 2 - Y ! THEN INKEY 54 = IF X @ 2 - X ! THEN INKEY 55 = IF X @ 2 + X ! THEN INKEY 56 = IF Y @ 2 + Y ! THEN X @ 20 > IF 20 X ! THEN Y @ 30 > IF 30 Y ! THEN X @ 1
 < IF 1 X ! THEN Y @ 1 < IF 1 Y ! THEN X @ A @ = IF Y @ B @ = IF ." GOT YOU!" 250 1000 BEEP QUIT THEN THEN X @ A @ > IF A @ 1+ A ! THEN X @ A @ < IF A @ 1– A ! THEN Y @ B @ > IF B @ 1+ B ! THEN Y @ B @ < IF B @ 1– B ! THEN X @ Y @ AT ." A" A @ B @ AT
." E" 250 70 BEEP 0 1 = UNTIL ;

Código original indentado

Una vez pasado a minúsculas e indentado para hacerlo más legible, parece otra cosa:

10 variable x
15 variable y
1 variable a
1 variable b

: chase
  cls
  10 x ! 15 y !
  1 a ! 1 b !
  begin
    x @ y @ at ."  "
    a @ b @ at ."  "
    inkey 53 = if y @ 2 - y ! then
    inkey 54 = if x @ 2 - x ! then
    inkey 55 = if x @ 2 + x ! then
    inkey 56 = if y @ 2 + y ! then
    x @ 20 > if 20 x ! then
    y @ 30 > if 30 y ! then
    x @ 1 < if 1 x ! then
    y @ 1 < if 1 y ! then
    x @ a @ = if
      y @ b @ = if
        ." got you!" 250 1000 beep quit
      then
    then
    x @ a @ > if a @ 1+ a ! then
    x @ a @ < if a @ 1– a ! then
    y @ b @ > if b @ 1+ b ! then
    y @ b @ < if b @ 1– b ! then
    x @ y @ at ." A"
    a @ b @ at ." E"
    250 70 beep
    0 1 =
  until
;

Simplificación

La primera posible simplificación que salta a la vista es trivial: El cálculo del final del bucle begin until es innecesario: En lugar de 0 1 = basta con 0.

La segunda simplificación también es muy sencilla: En lugar de imprimir una cadena de texto con un espacio para borrar los personajes:

x @ y @ at ."  "
a @ b @ at ."  "

Podemos usar la palabra space:

x @ y @ at space
a @ b @ at space

La tercera simplificación es mucho más interesante. Las coordenadas del personaje se actualizan con las siguientes cuatro estructuras condicionales:

inkey 53 = if y @ 2 - y ! then
inkey 54 = if x @ 2 - x ! then
inkey 55 = if x @ 2 + x ! then
inkey 56 = if y @ 2 + y ! then

Pero sus límites se comprueban después con otras cuatro estructuras condicionales:

x @ 20 > if 20 x ! then
y @ 30 > if 30 y ! then
x @ 1 < if 1 x ! then
y @ 1 < if 1 y ! then

Cuando todo ello podría hacerse de una sola vez de la siguiente forma:

inkey 53 = if y @ 2 - 1 max y ! then
inkey 54 = if x @ 2 - 1 max x ! then
inkey 55 = if x @ 2 + 20 min x ! then
inkey 56 = if y @ 2 + 30 min y ! then

Las palabras max y min hacen lo que necesitamos: limitar el rango de un valor. No hace falta usar estructuras condicionales para ello.

Pero aún no hemos terminado de simplificar: No hacen falta estructuras condicionales para actualizar las coordenadas según se haya pulsado o no cierta tecla, pues podemos usar directamente el resultado de la comprobación de cada tecla:

y @ inkey 53 = 2 * - 1 max y !
x @ inkey 54 = 2 * - 1 max x !
x @ inkey 55 = 2 * + 20 min x !
y @ inkey 56 = 2 * + 30 min y !

Y, como siempre que quitamos estructuras condicionales, podemos agrupar los cálculos y simplificar aun más:

y @ inkey 53 = 2 * - inkey 56 = 2 * + 1 max 30 min y !
x @ inkey 54 = 2 * - inkey 55 = 2 * + 1 max 20 min x !

Hemos pasado de ocho líneas de programa con ocho estructuras condicionales a dos líneas sin una sola estructura condicional... Eso es Forth.

De la misma forma podemos simplificar las instrucciones que calculan las coordenadas del misil que persigue al protagonista. El código original utiliza cuatro estructuras condicionales para sumar o restar uno a cada una de las dos coordenadas:

x @ a @ > if a @ 1+ a ! then
x @ a @ < if a @ 1– a ! then
y @ b @ > if b @ 1+ b ! then
y @ b @ < if b @ 1– b ! then

Pero, como hemos visto, para ese tipo de operaciones no hace falta usar estructuras condicionales, basta con usar directamente el resultado de los cálculos:

x @ a @ > a @ + a !
x @ a @ < negate a @ + a !
y @ b @ > b @ + b !
y @ b @ < negate b @ + b !

Pero gracias a haber eliminado las estructuras condicionales podemos simplificar más todavía y combinar los dos cálculos de cada coordenada en una sola operación:

a @  x @ a @ > +  x @ a @ < negate +  a !
b @  y @ b @ > +  y @ b @ < negate +  b !

Y aún es posible simplificar un poquito más:

a @  x @ over > +  x @ a @ < negate +  a !
b @  y @ over > +  y @ b @ < negate +  b !

Veamos ahora la condición que controla el final del juego:

x @ a @ = if
  y @ b @ = if
    ." got you!" 250 1000 beep quit
  then
then

Escribir la condición en un solo cálculo nos ahorrará otra estructura condicional (quizá a costa de una pérdida de velocidad, totalmente despreciable):

x @ a @ = y @ b @ = and
if ." got you!" 250 1000 beep quit then

Pero si el resultado de ese cálculo es el que indica el final del juego, ¿por qué no usarlo donde corresponde, para decidir el final del bucle principal?

En lugar de un bucle begin until cerrado que termina abruptamente con quit:

begin
  ( ... )
  x @ a @ = y @ b @ = and
  if ." got you!" 250 1000 beep quit then
  ( ... )
  0
until

Es más lógico, elegante y legible un bucle begin while repeat:

begin
  ( ... )
  x @ a @ = y @ b @ = and 0=
while
  ( ... )
repeat
." got you!" 250 1000 beep

Resultado

Con todos los cambios hechos hasta ahora, el código tiene este aspecto:

10 variable x
15 variable y
1 variable a
1 variable b

: chase
  cls
  10 x ! 15 y !
  1 a ! 1 b !
  begin
    x @ y @ at space
    a @ b @ at space
    y @ inkey 53 = 2 * - inkey 56 = 2 * + 1 max 30 min y !
    x @ inkey 54 = 2 * - inkey 55 = 2 * + 1 max 20 min x !
    x @ a @ = y @ b @ = and 0=
  while
    a @  x @ over > +  x @ a @ < negate +  a !
    b @  y @ over > +  y @ b @ < negate +  b !
    x @ y @ at ." A"
    a @ b @ at ." E"
    250 70 beep
  repeat
  ." got you!" 250 1000 beep
;

Como se aprecia, no queda ni una estructura condicional, todo lo hace con cálculos directos. Pero aún queda un paso más para que el código sea «auténtico» Forth: dividirlo en pequeñas palabras especializadas.

Dividir el código en partes

Tras haber reescrito el programa sin estructuras condicionales queda la fase más interesante: dividirlo en pequeñas palabras independientes. Esto sirve para evitar redundacias, para hacerlo más legible, más fácil de depurar y más fácil de mantener.

Por ejemplo, veamos el código que lee el teclado para actualizar las coordenadas del protagonista:

y @ inkey 53 = 2 * - inkey 56 = 2 * + 1 max 30 min y !
x @ inkey 54 = 2 * - inkey 55 = 2 * + 1 max 20 min x !

Si definimos una nueva palabra:

: key  ( c -- u )  inkey = 2 *  ;

Podemos reescribir el código de forma más clara:

y @ 53 key - 56 key + 1 max 30 min y !
x @ 54 key - 55 key + 1 max 20 min x !

Y mucho mejor si evitamos los «números mágicos» (números usados en el código que no muestran claramente su significado):

y @ ascii 5 key - ascii 8 key + 1 max 30 min y !
x @ ascii 6 key - ascii 7 key + 1 max 20 min x !

Pero seguir paso por paso el proceso sería muy largo, de modo que muestro el resultado terminado: Heat Seeker.