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.
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 dex
es mayor que cero y la tecla 5 está pulsada, entonces toma de nuevo el contenido dex
, réstale uno y guárdalo enx
.
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:
- El código original necesita 15 palabras para decirle a la máquina lo que tiene que hacer; la versión nueva necesita 11.
- El original emplea una estructura condicional para hacer o no hacer un cálculo, dependiendo del resultado de un cálculo anterior; la versión nueva hace un solo cálculo.
- El original usa tres menciones a la variable
x
; la versión nueva sólo una.
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.