Casting SPELs
Capítulo 4:
Echándole un vistazo al mundo de nuestro juego.
Vamos a querer movernos por nuestro mundo, para lo cual definiremos un par de útiles instrucciones. Lo primero que vamos a necesitar es información sobre el lugar en el que nos vayamos a encontrar. La instrucción describe_location , que nos dará la respuesta que buscamos, debe echarle un vistazo a nuestro mundo virtual y ser capaz de describir el lugar de nuestra localización.
describe_location(location, map):= second( assoc_(location, map))$
describe_location tiene como argumentos un lugar y un tablero de juego, dando como resultado la descripción exacta del sitio. La instrucción describe_location se comporta como una función matemática, y como tal no hace otra cosa que devolver valores de la función como respuesta. Con esto ya tenemos descrita nuestra primera función. Veamos con un poco más de detenimiento cómo trabaja.
Ya hemos hablado sobre las listas de asociación. La sentencia assoc_(location, map) nos da como respuesta la lista que tiene el nombre del lugar solicitado como primer elemento. Si le echamos un vistazo a map veremos que la descripción de los lugares ocupa en cada caso la segunda posición, por lo que al resultado de assoc_ debemos aplicarle después la función second . De esta manera construimos una sentencia compleja que se evaluará desde dentro hacia afuera. Se trata de la conocida operación de composición de la matemática elemental.
Antes de introducir la sentencia assoc_(location, map) en Maxima, debemos escribir brevemente la función assoc_ nosotros mismos. La función assoc que incorpora Maxima trabajaría correctamente con listas de asociación como object_locations , pero no admite más de dos elementos por lista. Se define así una versión adaptada a nuestro juego.
assoc_(key,alist):= block(
[ result:false ],
for elem in alist do
if key=first(elem) then return(result:elem),
result )$
Vemos aquí cómo se pueden escribir en Maxima funciones que contengan varias instrucciones. Con la ayuda de la función block se define un bloque de código, cuyo valor será el correspondiente al de su última sentencia, en nuestro caso, result . El primer argumento de block es una lista de variables locales. El símbolo de nombre result tan solo cobra sentido dentro del bloque, sin generar conflicto alguno con cualquier otro símbolo del mismo nombre fuera de la función assoc_ .
La instrucción do provoca un bucle. Mediante for elem in alist se crea una variable local elem que irá tamanado valores de la lista alist . Las instrucciones que siguen a do se ejecutan hasta el final del bucle, a menos que el bucle se interrumpa mediante la ejecución de una sentencia return , lo cual ocurrirá en nuestro caso cuando el primer elemento de elem coincida con la clave key . El valor devuelto por un bucle es o bien 'done , o bien el argumento de return . Sin embargo, de ninguno de estos dos valores se hace aquí uso. Es fundamental en nuestro caso que antes de la interrupción del bucle se haga la asignación result:elem .
Ahora deberíamos poner a prueba nuestra función describe_location
describe_location(location,map);
==> you are in the living_room of a wizards house. there is a wizard snoring loudly on the couch.
¡Perfecto! Esto es exactamente lo que queríamos.
Antes de que continuemos con nuestro juego, debemos hacer una breve pausa para aclarar el alcance de los símbolos que estamos empleando. Los dos símbolos location y map , que guardan información sobre nuestro juego, son variables globales, ya que las hemos definido en lo que podemos llamar el nivel superior. Estas variables son visibles desde todas partes, por lo que pueden ser invocadas desde cualquier lugar.
Sin embargo, los símbolos location y map que hemos utilizado en la definición de la función describe_location son locales y no tienen nada que ver con las variables globales de igual nombre. También podríamos haber definido describe_location mediante describe_location(l,m):= second( assoc_(l,m) )$ , con la ventaja de que las variables globales y locales utilizarían así símbolos diferentes, pero con la desventaja de que los símbolos l y m no serían muy explicativos sobre su contenido.
Con la sentencia describe_location(location,map); hacemos una llamada a la función describe_location , dándole como argumentos las correspondientes variables globales. La variable global location tiene actualmente el valor 'living_room , buscando entonces la función describe_location la correspondiente descripción de la sala de estar en map .
Otro detalle más. De haber escrito la definición describe_location():= second( assoc_(location,map) )$ , la llamada describe_location(); nos hubiese dado exactamente el mismo resultado. Así, describe_location no tendría argumentos y utilizaría directamente los valores de ambas variables globales. Una definición como esta no encajaría dentro de lo que se conoce como programación funcional, un concepto que tiene como objetivo que las funciones se escriban, en la medida de lo posible, según las siguientes pautas:
- Sólo los argumentos de las funciones, y las variables locales que se definan dentro de ellas, pueden ser utilizadas. Las variables globales no se leerán.
- Una vez asignado el valor a una variable, ya no se le cambiará.
- La interacción de una función con el resto del mundo se limita a tomar unos parámetros y devolver como resultado un valor.
Uno puede preguntarse si con unas limitaciones tan radicales es posible escribir programas que sean útiles; la respuesta es afirmativa, si uno se acostumbra a ellas. La base fundamental de este concepto es que de esta manera se asegura la transparencia referencial, lo que en la práctica se reduce a que podemos estar seguros de que una función con los mismos parámetros de llamada dará siempre el mismo resultado. Muchos fallos de programación pueden evitarse siguiendo este criterio.
Evidentemente, no siempre se pueden seguir estas pautas; por ejemplo, sería imposible almacenar cualquier tipo de contacto con el mundo externo, por lo que en este tutorial no todas las funciones van a cumplir estas reglas.
Volvamos nuevamente a nuestro juego. Necesitamos ahora una función que nos indique qué dirección debemos tomar para poder salir de nuestra ubicación actual y poder acceder a otro lugar. Para ello definimos una función que a partir de la descripción del camino que se encuentra en map forme frases legibles.
describe_path(path):=
sconcat("there is a ", path[2], " going ", path[1], " from here. ")$
La expresión path[n] hace referencia al n-ésimo elemento de la lista path ; esto es realmente práctico, ya que map[1][4][3] devolvería 'attic . La función sconcat transforma todos sus argumentos en una única cadena, de forma que obtenemos una frase ordenada en lengua inglesa.
describe_path('[west, door, garden]);
==> there is a door going west from here.
La información relativa a que este camino nos lleva al jardín la vamos a utilizar cuando realmente nos decidamos por tomar esta dirección. En describe_path debíamos suministrar una lista que fuese apropiada; sin embargo, vamos a definir ahora una función análoga a describe_location , que nos describa todos los caminos posibles a partir de nuestra localización actual.
describe_paths(location,map)=
apply( sconcat, map( describe_path,rest( assoc_(location,map),2 ) ) )$
Esta función es algo complicada. Empecemos por la parte más interna. Ya conocemos la expresión assoc_(location,map) , mientras que rest(list,2) aparta de la lista que obtenemos de assoc_ los dos primeros elementos. Con esto nos queda una lista con descripciones de rutas. Puesto que aquí no hay nada que sea pura teoría, tú mismo deberías comprobarlo en la práctica; es una de las ventajas de trabajar con un lenguaje interpretado, que se puedan ensayar rápida, interactiva y aisladamente pequeñas porciones de código.
Nos encontramos entonces en la sala de estar y tenemos de la lista devuelta por rest dos descripciones de caminos, pero en los otros dos casos una sola. Necesitamos ahora una función que aplique describe_path a cada uno de los caminos de la lista. Justo eso es lo que hace para nosotros la función map . Nos encontramos aquí con una función de orden superior; map es una función que toma como argumento otra función. De la misma manera tiene aquí apply a la función sconcat como argumento, haciendo que sconcat sea invocada con todos los elementos de la lista devuelta por map como argumentos.
describe_paths(location,map);
==> there is a door going west from here. there is a stairway going upstairs from here.
¡Genial!
Tan solo necesitamos ahora describir si allí donde nos encontremos actualmente existe algún objeto tirado en el suelo. En primer lugar escribimos la función is_at que nos va a indicar si un objeto determinado se encuentra en un lugar específico.
is_at(obj,loc,obj_loc):= block(
[ tmp:assoc_(obj,obj_loc) ],
listp(tmp) and is(second(tmp)=loc) )$
El valor devuelto por assoc_ puede ser, o bien false , o bien una lista, lo cual significa que antes de llegar a la función second debemos comprobar que se trata de una lista, para lo cual hacemos uso de la función listp . En el caso de que listp devuelva false se bloquea el operador lógico and para el resto de la sentencia. En caso de que assoc_ haya devuelto una lista, is nos dirá si la igualdad es correcta.
Muy bien, pongamos esto a prueba.
is_at('bucket,'living_room,object_locations);
==> true
¡Claro que el balde está en la sala de estar!
Hacemos uso de is_at para que nos describa todo lo que hay sobre el suelo.
describe_floor(loc,objs,obj_loc):=
apply( sconcat,
map( lambda([x],sconcat("you see a ",x," on the floor. ")),
sublist( objs, lambda([x],is_at(x,loc,obj_loc)) ) ))$
Veamos el resultado:
describe_floor('living_room,objects,object_locations);
==> you see a whiskey-bottle on the floor. you see a bucket on the floor.
Surgen entonces algunos otros objetos. Si se han olvidado los contenidos de objects y de object_locations , quizás no sea mala idea echar un vistazo más arriba.
El símbolo lambda es lo que se conoce como función anónima, con la cual definimos una función de forma local. Hubiésemos podido definir en lugar de describe_floor una función como blabla(x):= sconcat("you see a ",x," on the floor. ")$ ; el tercer renglón contendría entonces algo como map( blabla, ...) . El primer argumento de lambda es una lista con las variables de la función anónima, en este caso x .
El cuarto renglón devuelve una lista con los objetos de loc que están en el suelo. La función sublist es la que hace esto por nosotros, haciendo uso de una función de predicado que también se define como una lambda . Es el caso que sublist es una función para listas, como map , pero de orden superior.
Unimos ahora nuestras tres funciones descriptoras en una sola instrucción:
l_o_o_k():=
sconcat( describe_location(location,map),
describe_paths(location,map),
describe_floor(location,objects,object_locations) )$
Desde luego esta no es ninguna función en el sentido del análisis funcional, ya que l_o_o_k no tiene parámetros de entrada y lee variables globales. La respuesta que nos da es dónde nos encontramos, y esto ya nos vale.
Sin duda podemos llamar a la función sin más que hacer l_o_o_k(); , pero vamos a echar mano de un pequeño truco para no tener que escribir más que look; .
Maxima nos ofrece la posibilidad de definir operadores. Definimos ahora el operador look sin argumentos.
nofix("look")$
"look"():= l_o_o_k()$
Y hasta aquí hemos llegado
look;
==> you are in the living-room of a wizard's house. there is a wizard
snoring loudly on the couch. there is a door going west from here. there is a stairway going
upstairs from here. you see a whiskey-bottle on the floor. you see a bucket on the floor.
¡Esto es lo máximo!
Nosotros queremos movernos por nuestro mundo, visitar otros lugares y llevar a cabo un par de acciones. Al final nos espera un magic low_carb donut como recompensa.
Introducción |
Capítulo 1 |
Capítulo 2 |
Capítulo 3 |
Capítulo 4 |
Capítulo 5 |
Capítulo 6 |
Capítulo 7 |
Agradecimientos |
Licencia
© Mario Rodríguez Riotorto, 2006
|