Uso del compiladorInform como lenguaje de programaciónSiete estructuras de datos especialesEl lenguaje de los Objetos

El lenguaje de los Objetos

Objetos y comunicación

Los objetos en un programa son sus partes constituyentes: pequeñas agrupaciones de código y datos. El punto de partida de un "lenguaje orientado a objetos" es que un buen diseño debe agrupar fragmentos de información en paquetes, junto con los fragmentos de programa que manejan esa información. Pero la idea va más allá:

  1. Un objeto es algo con lo que puedes comunicarte. (Es como una compañía en la que mucha gente trabaja en el mismo edificio, compartiendo la misma dirección: para el resto del mundo aparecen como una sola persona.)
  2. La información dentro del objeto debe permanecer oculta para el mundo exterior (lo mismo que los archivos secretos de una compañía). Esto a veces se denomina "encapsulación".
  3. El mundo exterior puede pedirle al objeto que haga algo, pero no necesita saber cómo lo hará. (La compañía puede decidir un día cambiar su estrategia de almacenaje, pero el mundo exterior ni siquiera se dará cuenta de que esto ha ocurrid, aunque pueda ser un cambio dramático internamente.)

Estos tres principios ya se han usado con las rutinas: (1) puedes llamar a una rutina, pero no puedes llamar a "sólo esta parte de la rutina" (esto es, aunque la rutina es un agregado de instrucciones, se comporta como una sola cosa); (2) Las variables locales de una rutina son su propiedad privada, y el resto del programa no puede averiguar ni alterar sus valores; (3) Mientras la rutina haga la misma tarea, puede ser reescrita por completo y el resto del programa seguirá funcionando como si no hubiera habido cambios.

¿Por qué molestarse con estos detalles filosóficos? Hay dos respuestas. En primer lugar, Inform fue diseñado para hacer aventuras conversacionales, en las cuales los objetos son una idea muy adecuada para representar cosas y lugares del juego. En segundo lugar, la filosofía de "objetos" es una forma de organizar los programas largos y complicados.

Otra idea clave es la comunicación. Uno puede imaginar que un programa es un gran grupo de compañías, que constantemente se telefonean para solicitar información o para pedir que se hagan cosas. En un "mensaje" típico, un objeto A envía una pregunta o instrucción detallada a otro objeto B, el cual replica con una respuesta simple. (De nuevo, esto ya lo hemos visto con las rutinas: una rutina llama a otra, y la otra devuelve un resultado.)

Las rutinas son, por tanto, un tipo de objeto. Pero hay en total cuatro tipos de objeto en Inform, que son:

Estos cuatro tipos de objeto se llamaan "metaclases". Si Obj es un objeto, entonces la función

    metaclass(Obj)

nos dirá siempre qué tipo de objeto es, siendo la respuesta uno de los siguiente valores (que son constantes internas):

    Routine   String   Object   Class  

Por ejemplo,

metaclass("Concierto para Violín n. 1")  

nos devolverá String, mientras que

    metaclass(Main)  

debe retornar siempre Routine (ya que Main es el nombre de una rutina que indica dónde debe comenzar a ejecutarse un programa Inform). Ya hemos visto antes con detalle las metaclases Routine y String (aunque entonces no sabíamos que eran metaclases). Así que ahora nos concentraremos en las otras dos metaclases Object y Class.

(!)(!) ¿Por qué sólo esos cuatro tipos? ¿Por qué las cadenas (strings) son objetos, y sin embargo no lo son, por ejemplo, las variables o las palabras de diccionario? Los lenguajes orientados a objetos ofrecen una gran diversidad en cuanto al extremo hasta el que llevan la noción de objeto: en el dogmático Smalltalk-80, todos los ingredientes de cualquier tipo de un programa son llamados objetos: el propio programa, el número 17, cada variable, etc. Inform es mucho más moderado. Las rutinas, los objetos y las clases tienen claramente características de objeto, y en cuanto a las cadenas (strings), resulta que es bastante conveniente tratarlas como objetos también (como veremos después). Pero Inform se detiene aquí.

Funciones internas 2: el árbol de objetos

Las rutinas, las cadenas (strings) y, como ya veremos, las clases, están dispersas a lo largo de un programa Inform, sin ningún orden en particular, y nada relaciona unas con otras. Los objetos de tipo Object son especiales, porque se hallan todos interrelacionados en el llamado "árbol de objetos".

En este árbol, los objetos tienen relaciones de tipo "parentesco" entre unos y otros: cada objeto tiene un padre (parent), un hijo "directo" (child) y un hermano (sibling). Normalmente, el padre, el hijo "directo" y el hermano de un objeto es otro objeto del árbol, pero también puede ser

    nothing

que significa "ningún objeto". Por ejemplo, mira este árbol:

    Pradera 
      ! 
    Buzon  ->  Jugador
      !            ! 
    Nota         Cetro   ->   Pepino  ->   Linterna  ->   Varita Magica          
                                              !
                                            Bateria  

El Buzon y el Jugador, son ambos hijos (children) de la Pradera, que es su padre (parent), pero sólo el Buzón es hijo "directo" (child) de la Pradera. La Varita Magica es hermana (sibling) de la Linterna, la cual es a su vez hermana (sibling) del Pepino, etc. La Nota tiene a nothing como hijo directo, y tambien como hermano.

Inform proporciona funciones especiales para averiguar cosas sobre el árbol. Las funciones son los nombres en inglés de estas relaciones de parentesco, es decir: parent, sibling y child. Además, tiene una función llamada children que cuenta el número de hijos que tiene un objeto dado (es decir, cuantos suman entre su child, el hermano de éste, el hermano de este hermano, y así hasta terminar la cadena de hermanos). Veamos unos ejemplos:

     parent(Buzon)     == Pradera
     children(Jugador) == 4
     child(Jugador)    == Cetro
     child(Cetro)      == nothing
     sibling(Linterna) == Varita Magica

No es una buena idea tratar de aplicar estas funciones al objeto nothing (ya que, de hecho, no se trata de un objeto, sino de un valor que representa la ausencia de objeto). Pero en Inform todos los objetos son representados internamente por cantidades, incluso nothing es una cantidad. Así que si tenemos una variable X que almacena una cantidad ¿cómo saber si esa cantidad representa a un objeto válido (y por tanto puede aplicársele parent, child, etc.) o no lo representa (y por tanto no se le puede aplicar ninguna de esas funciones)? La respuesta es que la llamada a

    metaclass(X)  

retorna nothing para cualquier valor de X que no sea un objeto válido. En particular, lo siguiente es cierto:

    metaclass(nothing)  == nothing  

(!) Esperemos que haya quedado claro por qué el árbol de objetos es muy útil a la hora de escribir aventuras conversacionales: proporciona una forma de represntar la idea fundamental de que una cosa está contenida en otra. Pero incluso para programas que no sean juegos de aventura puede ser un concepto útil. Por ejemplo, es una forma eficiente de almacenar estructuras arborescentes y listas enlazadas.

Creación de objetos 1: construyendo el árbol

El estado inicial del árbol de objetos se crea mediante la directiva Object. Por ejemplo,

Object "caldero" ...
Object -> "estrella de mar" ...
Object -> "ostra" ...
Object -> -> "perla" ...
Object -> "arena" ...

(donde hemos omitido la definición del objeto, sustituyéndolo por "..."), crea el siguiente árbol de objetos

           "caldero"
              !
          "estrella de mar" -> "ostra" -> "arena"
                                  !
                               "perla"

La idea es que si detrás de la directiva Object no aparece ninguna flecha (->), entonces el objeto no tiene padre. Si aparece una ->, entonces su padre es el último objeto que haya sido definido sin flechas. Si aparecen dos, entonces su padre es el último objeto que haya sido definido con una flecha, etc. (La lista de definiciones se parecerá un poco a un dibujo del árbol girado de lado).

La definición de un objeto consiste en una "cabecera" seguida de un "cuerpo", el cual a su vez se divide en "segmentos" (aunque aquí termina el parecido con los gusanos). La cabecera tiene la forma siguiente:


Object <flechas> <nombre_interno> "nombre textual" <padre>

pero estos cuatro elementos son todos ellos opcionales.

  1. La parte de las <flechas> ya se ha descrito. Observar que si usamos flechas, ya estamos especificando cuál es el padre de este objeto, por lo que en este caso no podemos usar <padre> también.
  2. El <nombre_interno> es el que usará el programador para referirse a este objeto dentro del programa. Es análogo al nombre de una variable. Como en los nombres de variables, no puede usarse aquí eñes ni acentos, pero no importa porque el jugador nunca sabrá cómo hemos llamado internamente al objeto.
  3. El "nombre textual" debe suministrarse si en algún momento el programa necesita imprimir el nombre de este objeto. Si se suministra, se imprimirá esa cadena de texto, la cual puede contener acentos, eñes, espacios...
  4. El <padre> es el nombre interno de otro objeto, que ha tenido que declararse antes en el programa. Es una alternativa a las flechas.

Todo lo anterior es opcional. De hecho, incluso una línea así:

Object;

está permitida, aunque lo que hace es crear un objeto sin nombre y sin características, cuya utilidad es más bien dudosa.

Instrucciones para objetos: move, remove, objectloop

Las posiciones de los objetos en el árbol no son fijas. Cuando los objetos se crean, lo hacen en una formación particular, pero a menudo se mueven y reordenan intensivamente durante la ejecución del programa. (En un juego de aventuras, en el que los objetos representan cosas y habitaciones, los objetos se mueven por el árbol cada vez que el jugador coge algo o se mueve a otra localidad.) La instrucción


move <objeto> to <objeto>

mueve el objeto mencionado en primer lugar, de modo que se convierte en el hijo "directo" (child) del objeto mencionado en segundo lugar. Todos los objetos que eran hijos del segundo, siguen siéndolo, si bien se "desplaza" el que era hijo directo. Por ejemplo, si partimos del árbol que ya hemos presentado antes:

    Pradera 
      ! 
    Buzon  ->  Jugador
      !            ! 
    Nota         Cetro   ->   Pepino  ->   Linterna  ->   Varita Magica          
                                              !
                                            Bateria  

y a partir de él ejecutamos la instrucción

move Pepino to Buzon;  

la nueva situación sería el árbol siguiente

  
    Pradera
      ! 
    Buzon -------------> Jugador
      !                     ! 
    Pepino  ->  Nota      Cetro  ->  Linterna  ->  Varita Magica
                                        !
                                      Bateria

Hay que remarcar que move no imprime nada en la pantalla, y de hecho no hace nada más que reordenar el árbol. Cuando un objeto se convierte en el hijo de otro de esta forma, siempre se convierte en el hijo "directo", es decir, es el nuevo child() de su padre, "empujando" a todos los hijos previos, y convirtiéndolos en sus hermanos (siblings). Es ilegal mover un objeto fuera del arbol mediante una instrucción como

    move Linterna to nothing;

porque nothing no es realmente un objeto. Para sacar un objeto del árbol lo que debe hacerse es:

    remove Linterna;  

lo que daría como resultado el árbol

    Pradera                                              Linterna
      !                                                    !
    Buzon ------------> Jugador                          Bateria
      !                    ! 
    Pepino  ->  Nota     Cetro  ->  Varita Magica

De modo que el "árbol de objetos" queda roto en dos arbolitos. Es frecuente que esto ocurra durante la ejecución de un juego.

Ya que los objetos se mueven por ahí un montón, es útil poder comrpbar dónde está un objeto en un momento dado; para esto existe la condición in. Por ejemplo,

    Pepino in Buzon

es verdadero (true), sólo si Pepino es un hijo del Buzon (tanto si es hijo "directo" como si es uno de los hermanos de ese hijo "directo", pero no si es hijo de un hijo del buzón. Es decir, sería falso si es "nieto" del buzón, dicho de otro modo Pepino in Buzon es cierto, pero Pepino in Pradera es falso). Observar que en realidad la condición

    X in Y  

no es más que una abreviatura de

    parent(X) == Y  

pero merece la pena tenerla porque se usa muy a menudo.

La instrucción para hacer bucles que nos habíamos saltado en la sección * era objectloop, que se usa así:


objectloop(<nombre_de_variable>) <instrucción>

y lo que hace es ejecutar la <instrucción> (que puede ser un bloque de instrucciones entre llaves) una vez para cada objeto del árbol, asignando a la variable <nombre_de_variable> un objeto diferente del árbol en cada repetición. Por ejemplo,

objectloop(x) print (name) x, "^";  

imprime una lista con los nombres textuales de todos los objetos del árbol. (Si algún objeto no ha definido su nombre textual, entonces aparecerá listado como "?").

Un caso más interesante y más potente consiste en escribir una condición dentro de los paréntesis. Esta condición debe comenzar con un nombre de variable. Por ejemplo:

objectloop(x in Buzon) print (name) x, "^";  

imprime sólo los nombres de los objetos que satisfacen la condición, esto es, los hijos del Buzon.

Cuando se usa entre paréntesis una condición como esa, es importante que la <instrucción> ejecutada por objectloop no modifique el árbol. Por ejemplo, si tratamos de destruir todos los objetos del buzón con algo como esto:

objectloop (x in Buzon) remove x;  

el programa fracasará miserablemente. La razón es que Inform intenta ser "demasiado listo" y convierte este objectloop en un bucle for que no necesite recorrer todo el árbol del juego, sino sólo una pequeña parte. En particular, este ejemplo resulta convertido por Inform en este otro código:

 for (x=child(Buzon):x!=nothing:x=sibling(x)) remove x; 

La conversión de objectloop en for no causa problemas mientras la <instrucción> no haga cambios en la estructura del árbol. Pero en este caso sí los hace, y el bucle no funcionará. En efecto, el primer valor tomado por x sería Pepino, pero una vez que Pepino es eliminado del árbol por la instrucción remove x, ¡deja de tener hermanos!

En general, no es buena idea cortar las ramas de un árbol mientras uno está trepando por él. La forma correcta para lograr eliminar los hijos del buzón sería esta:

  objectloop (x) {
    if (x in Buzon) remove x;
  }

En este caso Inform ya no intenta convertir el objectloop en un for, ni usará la función slibing para moverse por el árbol. Simplemente irá dando a x los valores de todos los objetos del juego, y ejecutará lo que hay entre las llaves para cada valor de x. Es ahí donde comprobamos si x es un hijo del buzón, en cuyo caso lo eliminamos. Esta forma de proceder es segura, pero a cambio es más lenta que la otra, porque x pasa por todos los objetos del juego.

Creación de objetos 2: with propiedades

Hasta ahora los Object son simplemente elementos con un nombre enganchado, que pueden ser mareados arriba y abajo por un árbol. Se vuelven realmente interesantes cuando podemos engancharles datos y rutinas, y para esto es para lo que sirve el "cuerpo" de un objeto.

El cuerpo contiene un máximo de cuatro segmentos, que pueden aparecer en cualquier orden, y todos ellos son opcionales. Los segmentos se llaman

    with    has    class    private  

Dejaremos class para más tarde. El segmento más importante es with, que especifica cosas que van a "engancharse" al objeto. Por ejemplo

Object urraca "pájaro con bandas negras"
  with envergadura, gusanos_tragados;  

"engancha" dos variables a la urraca, una llamada envergadura y la otra llamada gusanos_tragados. Observa que si se pone más de una variable, se usan comas para separarlas, y que la definición del objeto completo termina con un punto y coma, como siempre. Para acceder a los valores de las variables de la urraca desde cualquier otro punto del programa, se usa la sintaxis

    urraca.envergadura
    urraca.gusanos_tragados

que puede ser usada en la misma forma que cualquier variable (global). Observa que no solo se pone el nombre de la variable, sino que también debe ponerse delante el objeto a que pertenece, por lo que

    aguila.envergadura
    urraca.envergadura      

son variables diferentes.

Las variables que están "enganchadas" a los objetos de esta forma se denominan "propiedades". Precisando aún más, diremos que la palabra envergadura es una "propiedad" y que es "proporcionada" por los objetos urraca y aguila.

Para saber si un objeto "proporciona" una propiedad en particular, se puede usar la condición provides. Por ejemplo,

    objectloop (x provides envergadura) ...  

ejecuta el código ... para cada objeto x del juego que tenga la propiedad envergadura.

(!) Esta forma de objectloop es también segura frente a modificaciones en el árbol, puesto que el compilador la convierte en:
    objectloop (x) { if (x provides envergadura) ... }:    

Al hacer la definición del objeto urraca que hemos visto más arriba, sus propiedades urraca.envergadura y urraca.gusanos_tragados reciben ambas el valor cero. Para crear una urraca con una envergadura dada, tenemos que especificar un valor inicial. Esto se hace sumininstrando uno después del nombre de la propiedad, por ejemplo:

Object urraca "pájaro con bandas negras"
  with envergadura 5, gusanos_tragados;

y en este caso el programa comenzará con urraca.envergadura igual a 5, y urraca.gusanos_tragados aún igual a cero. (Quizás pienses que por consistencia, para hacerlo igual a las variables globales, debería haber un signo igual delante del 5. Tienes razón, pero Graham Nelson ha preferido quitar el signo igual en caso de propiedades, porque de otro modo "los programas en Inform estarían horriblemente llenos de signos igual".)

(!) Las propiedades pueden ser arrays en lugar de simples variables. Si en lugar de dar un solo valor a la propiedad se le da más de uno separados por espacios), entonces la propiedad se convierte en un array. Así,

Object urraca "pájaro con bandas negras"
  with nombre 'urraca' 'pajaro' 'con' 'bandas' 'negras',
       envergadura 5,
       gusanos_tragados;  

En este caso urraca.nombre no se usa como una variable global, no tiene sentido, por ejemplo, sumarle 1. Es en realidad un array tipo word, pero de una clase un poco especial. Para poder acceder a los elementos de este array debemos usar unos operaadores especiales: .# y .&

    urraca.&nombre  

significa "el array que está almacenado en la propiedad nombre del objeto urraca", de manera que para acceder a los elementos de ese array necesitamos escribir

    magpie.&name-->0
    magpie.&name-->1
       ...
    magpie.&name-->4

El número de elementos de este array puede descubrirse con

    urraca.#nombre

que nos devuelve el doble del número de elementos (en este caso, devolvería 10). La razón por la que devuelve el doble es que realmente lo que devuelve es el número de bytes ocupados por el array, y ya que se trata de un array tipo word, cada elemento ocupa 2 bytes.

(!)

La propiedad nombre es un alias definido por InformATE para que equivalga a la propiedad name. A su vez, la propiedad name es creada por el compilador de Inform, es una especie de palabra reservada. Esta propiedad (por estar definida internamente) se diferencia de las restantes en una sola cosa: si los valores de esta propiedad aparecen entre comillas dobles, Inform los trata como si hubieran aparecido entre apostrofes, es decir, los incluye en el diccionario del juego en lugar de tratarlos como un string. Por tanto, en esta propiedad (pero sólo en esta), la lista de palabras puede ir entre comillas dobles o entre apóstrofes.

Se recomienda de todas formas usar siempre apóstrofes por dos razones: en primer lugar queda mucho más claro y más consistente con el resto del programa, que los valores de esta propiedad son palabras de diccionario, y no strings; y en segundo lugar otras propiedades de la librería InformATE, como nombre_m, nombre_f, adjetivos, etc... no comparten esta característica, por lo que en éstas no se pueden poner las palabras entre comillas. Así pues, para homogeneizar las propiedades de objetos, mejor usar siempre apóstrofes 'así'.

Por último, las propiedades también pueden ser rutinas. En la definición siguiente

Object urraca "pájaro con bandas negras"
  with nombre 'urraca' 'pajaro' 'con' 'bandas' 'negras',
       envergadura 5,
       fuerza_sustentacion [;
          return urraca.envergadura + urraca.gusanos_tragados;
       ],
       gusanos_tragados;

urraca.fuerza_sustentacion no es ni una variable ni un array, sino una rutina, escrita entre corchetes como es habitual. (Observar que la definición del objeto continúa después del delimitador de final de rutina ].) Las rutinas como esta que son propiedad de un objeto, se dice que están "embebidas", y se usan fundamentalmente para recibir mensajes (como veremos).

(!)(!) Las rutinas embebidas se diferencian de las normales en dos aspectos:
  1. El nombre de la rutina embebida es como el nombre de una propiedad, es decir, debe llevar delante el objeto al que pertenece. Por ejemplo, se ejecutaría esa rutina llamándola así:
    urraca.fuerza_sustentacion().
  2. Si la ejecución de una rutina embebida llega a alcanzar su marca de fin ], entonces la rutina termina y devuelve false, y no true como haría una rutina no embebida. La razón para este comportamiento es que resulta mucho más cómodo para la programación de las rutinas embebidas de InformATE llamadas antes y despues.

Propiedades privadas y encapsulación

(!) Se proporciona un mecanismo opcional para la "encapsulación" de ciertas propiedades, de modo que solamente las rutinas embebidas en el propio objeto puedan consultar y modifica estas propiedades. Estas propiedades se definen en un segmento del objeto llamado private. Por ejemplo:

Objeto centinela "centinela"
  private  numero_clave  16399,
  with     reto [ intento ;   ! Es una variable local a esta rutina
             if (intento==centinela.numero_clave)
                "¡Acérquese, amigo!";
             "Quieto ahí, forastero.";
           ];

hace que el centinela tenga dos propiedaes: reto, que es pública, y numero_clave, que es privada y por tanto sólo puede ser usada desde las rutinas embebidas del centinela.

(!)(!) Esto hace la condición provides un poco más interesante de lo que parecía en la sección precedente. La respuesta a la condición
    centinela provides numero_clave   

depende de quién haga la pregunta: es true si la comprobación se hace desde alguna de las rutinas embebidas del centinela, pero false en otro caso. Una propiedad privada está tan bien protegida que nadie puede ni tan siquiera saber si existe o no.

Atributos: give y has

Además de las propiedades, los objetos pueden llevar "enganchados" variables de tipo "bandera" (flag). Recuerda que este tipo de variables sólo pueden tomar dos valores: true o false: la bandera está ondeando o no).

Aunque podrías usar una propiedad normal para asignarle el valor true o false, esto sería un desperdicio de memoria, ya que las propiedades normales (como urraca.envergadura) están preparadas para contener datos de 16 bits, mientras que una bandera sólo necesita un bit. Por esto, existe un segmento específico dentro de los objetos para especificar propiedades de tipo "bandera". A este tipo de propiedades se las denomina atributos.

Si bien las propiedades pueden "engancharse" a los objetos simplemente incluyéndolas en el segmento with del objeto, los atributos en cambio deben ser declarados antes de ponerlos en un objeto. Para ello se usa la directiva Attribute, como por ejemplo en:

Attribute aburrido;  

Una vez que se ha hecho esta declaración, automáticamente todos los objetos del juego tendrán un atributo llamado aburrido, que puede tomar el valor true o false en un momento dado para cada objeto. Para saber si se halla en el estado true, existe la condición has (que puede traducirse por "es" o "tiene"), por ejemplo:

if (urraca has aburrido) ...

comprueba si la urraca tiene el atributo aburrido activado (esto es, con el valor true), puede leerse como "Si la urraca está aburrida..."

Por defecto todos los objetos reciben este atributo con el valor false, pero al crear un objeto podemos cambiar esto y hacer que comienze con el atributo a true. Para ello existe el segmento llamado has dentro de la declaración del objeto. En este segmento se escriben, separados por espacios, los nombres de todos los atributos que deseamos empiecen con el valor true para ese objeto. Por ejemplo:

Object urraca "pájaro con bandas negras"
  with envergadura, gusanos_tragados
  has  aburrido;

El segmento llamado has (observar que es la misma palabra que se usa para comprobar si el objeto tiene ese atributo activo), contiene la lista de los atributos que queremos activar, si bien podemos poner el signo ~ delante de uno de ellos, en cuyo caso nos aseguramos de que ese atributo toma el valor false (el signo ~ representa la negación). Como hemos dicho, no es necesario normalmente especificar los atributos que comienzan valiendo false, puesto que este es el valor por defecto, pero si el objeto "hereda" el atributo activado de una clase, de este modo lo desactivaríamos de nuevo (más sobre herencia en el apartado siguiente).

Finalmente, el estado del atributo puede modificarse en cualquier momento del juego, mediante la instrucción give (que significa "dar"), así por ejemplo:

give urraca aburrido;

pone true en el atributo aburrido de la urraca. Para ponerlo a false basta poner un ~ delante del nombre del atributo, como por ejemplo:

give urraca ~aburrido;  

La instrucción give puede recibir una lista de atributos, en cuyo caso pone a true todos ellos (excepcto los que lleven un ~ delante que son puestos a false). Por ejemplo:

give Reja ~cerrojoechado abierta;

significaría "quita el atributo cerrojoechado de la Reja, y ponle el atributo abierta" (Estos son atributos usados por la librería InformATE. Para mayor comodidad del programador, esta librería define el atributo abierta como un sinónimo del atributo abierto).

Clases y herencia

Una vez que hemos tratado las rutinas y las cadenas en la sección sobre el lenguaje de las rutinas, y los objetos en los apartados anteriores, la cuarta y última metaclase que nos queda por estudiar es la "clase". Una clase es una especie de objeto-prototipo, que se usa como "molde" para generar otros objetos idénticos (o parecidos) a él. Estos objetos "copiados" de la clase, a menudo se llaman "instancias" o "miembros" de la clase, y se dice que "heredan de" ella.

Por ejemplo, todos los pájaros deberían tener la propiedad "envergadura" y también la propiedad "fuerza de sustentación", que sería, como hemos visto una función, que en el caso de la urraca era:

   fuerza_sustentacion [;
      return urraca.envergadura + urraca.gusanos_tragados;
   ],

que usa una fórmula para calcular la fuerza de sustentación en función de la envergadura de la urraca y de los gusanos que ha comido. Si nuestro juego incorpora varios pájaros diferentes, y todos ellos necesitan tener estas propiedades, puede ser pesado escribirlo una y otra vez en todos ellos. Es mejor crear una "clase" llamada Pajaro que sirva como molde. Las clases se definen con la directiva Class, por ejemplo así:

Class Pajaro
 with envergadura 7,
      gusanos_tragados,
      fuerza_sustentacion [;
        return self.envergadura + self.gusanos_tragados;
      ];         

y después crear diferentes tipos de pájaro "copiados" (o sea, "derivados") de esta clase. Por ejemplo, una urraca, un halcón, un águila y un buho:

Pajaro "urraca"
  with envergadura 5;
Pajaro "halcón";
Pajaro "águila"
  with envergadura 15;
Pajaro "buho"
  with  gusanos_tragados 1;

Hay algunas cosas interesantes en todo lo anterior. En primer lugar, la directiva Class define una nueva clase llamada Pajaro. Cada miembro de esta clase tendrá automáticamente una propiedad envergadura con valor 7, una propiedad gusanos_tragados con valor 0, y una rutina fuerza_sustentacion que suma ambos valores para calcular la fuerza.

Después, las definiciones de los diferentes pájaros se hacen en forma idéntica a la definición de un objeto, sólo que sustituyendo la directiva Object por el nombre de la clase a que pertenecen, en este caso Pajaro. En realidad esto es una forma de abreviar la verdadera sintaxis, que sería esta otra:

Object "urraca"
 class Pajaro
  with envergadura 5;

donde class (atención, ¡con la inicial en minúscula!) es el segmento que nos quedaba por explicar. En el segmento class se escribe una lista de las clases de las cuales deriva el objeto, ya que, como veremos, un objeto puede ser una "mezcla" de varias clases.

Fíjate ahora que la rutina fuerza_sustentacion de la clase Pajaro debe ser escrita de modo que pueda ser aplicada a cualquier objeto derivado. Debe decir "la fuerza de sustentación de cualquier pájaro es igual a su envergadura más el número de gusanos que haya comido". Para lograr expresar esto, usamos la palabras especial self, que significa "el objeto que se esté considerando cuando esto se ejecute". Así, self tomará el valor urraca cuando se esté ejecutando urraca.fuerza_sustentacion(). Vemos más sobre esto en el siguiente apartado.

Fíjate también que la clase Pajaro especifica un valor inicial de 7 para envergadura. Este es el valor que "heredarán" todos los miembros a menos que en sus definiciones proporcionen un valor diferente que lo sustituya, como hacen los objetos urraca y aguila. Así que, una vez creados los cuatro pájaros anteriores, los valores iniciales de sus propiedades serían:

Pájaro envergadura gusanos_tragados
urraca 5 0
halcon 7 0
aguila 15 0
buho 7 1

(!)(!) En casos muy raros, los conflictos entre lo que dice la clase y lo que dice el objeto se resuelve de forma diferente. Véase la sección sobre mensajes y clases.

Inform permite la "herencia múltiple", lo que significa que un objeto puede heredar de cualquier número de clases. Por tanto, un objeto no es miembro de una única clase, sino que puede serlo de varias clases a la vez.

Cada objeto es miembro de al menos una clase, debido a que las cuatro "metaclases" (Routine, String, Object y Class) son a su vez clases. Por tanto todos los objetos son miembros, como mínimo, de la clase Object. La urraca del ejemplo anterior es miembro a la vez de las clases Object y Pajaro.

Para complicar más aún las cosas, las clases también pueden ser miembro de otras clases:

Class  AveDePresa
 class Pajaro
 with  envergadura 15,
       personas_tragadas;

AveDePresa cernicalo;

crea un cernicalo que es miembro a la vez de las clases AveDePresa y Pajaro. Informalmente se dice que AveDePresa es una "subclase" de Pajaro

Puesto que un objeto pertenece a varias clases a la vez, sería imposible tener una función class(obj) que nos diga a que clase pertenece, de forma análoga a como funcionaba metaclass(). En lugar de esto, lo que tenemos es una condición llamada ofclass que nos dice si el objeto es miembro o no de una clase dada. Por ejemplo:

    cernicalo ofclass String

sería falso (false), mientras que

    cernicalo ofclass Pajaro
    cernicalo ofclass AveDePresa
    cernicalo ofclass Object
    "Canterbury" ofclass String

son todas ciertas (true). Esta condición es muy útil para ser usada con objectloop:

objectloop (x ofclass Pajaro) move x to Jaula;      

mueve todos los pájaros del programa a la Jaula.

Mensajes

Hasta aquí hemos visto todo sobre cómo crear objetos, y es el momento de comenzar a comunicarlos entre sí por medio de los mensajes. Cada mensaje tiene un emisor, un receptor y algunos parámetros enganchados, y siempre obtiene una respuesta (que es simplemente un valor). Por ejemplo,

x = lampara.echarAceite(5, 80);  

envia el mensaje echarAceite al objeto lampara, con los parámetros 5 y 80. La respuesta a este mensaje es almacenada en la variable x. Los mensajes son recibidos por las rutinas embebidas de los objetos. En realidad, el envío de un mensaje puede verse como una llamada a una rutina embebida. La respuesta del mensaje es lo que llamábamos el "valor retornado" por una rutina, y los parámetros del mensaje son como los parámetros que se pasan a las rutinas. Sin embargo, hay algunos detalles más, como se pondrá de manifiesto enseguida.

¿Qué hace que el objeto lampara responda a éste mensaje? En primer lugar, el programador debe haber especificado una rutina embebida en ese objeto, llamada echarAceite. Si no lo ha hecho, cuando el programa sea ejecutado, aparecerá un mensaje de error en el instante en que este mensaje se intente enviar, del estilo de:

** Rin-time error: lampara (object number 6) has no proprerty
    echarAceite to send message **

Este mensaje, que sale en inglés porque es generado por unas rutinas que Inform añade automáticamente al programa y que no forman parte de la librería InformATE, nos indica que el objeto lampara no tiene la propiedad echarAceite y por tanto no puede recibir el mensaje que intentamos enviarle.

La propiedad lampara.echarAceite no solo debe existir, sino que además debe ser uno de los cuatro tipos básicos (metaclases) o bien el valor 0, o lo que es lo mismo Nothing. Lo que ocurre en concreto cuando se intenta enviar el mensaje, depende a qué metaclase pretenezca lampara.echarAceite:

Metaclase Qué ocurre Cuál es la respuesta
Routine Se llama a la rutina con los parámetros dados El valor retornado por la rutina
String Se imprime el texto contenido en la propiedad, seguido de un retorno de carro. Se ignoran los parámetros true
Object Nada el objeto contenido en esa propiedad
Class Nada La clase contenida en esa propiedad
Nothing Nada false (o lo que es lo mismo, 0)

(!) Si la propiedad contiene una lista de valores, en lugar de uno solo, entonces se considera sólo el primer valor de la lista, al que se aplican las reglas anteriores.

Por ejemplo,

print cernicalo.fuerza_sustentacion();

imprimirá 15, puesto que fuerza_sustentacion en el cernicalo es una rutina (heredada de la clase Pajaro), que al ejecutarse suma el valor de envergadura (15) con el valor de gusanos_tragados (0) del cernicalo. Por tanto, la rutina retorna 15, y esta es la respuesta del mensaje, que es impresa por print. (Puedes observar todos los mensajes que se envían entre objetos durante la ejecución de un juego si escribes el verbo MENSAJES durante el juego. Mira la sección sobre depuración y traza para más detalles.)

(!) Por ver más ejemplos de mensajes cuando la propiedad que los recibe no es una rutina, he aquí lo que ocurre aproximadamente cuando la librería InformATE trata de mover al jugador hacia el noroeste, desde la localización actual, en una aventura

x=localizacion.al_ne();
if (x==nothing) "No puedes ir por ahí.";
if (x ofclass Object) move jugador to x;

Esto permite que las salidas de una localidad se puedan dar con bastante flexibilidad en propiedades como al_ne, ya que cuando se envía el mensaje localizacion.al_ne(), la propiedad al_ne puede ser una cadena (que resultará impresa), otro objeto (es decir, otra localidad) que será la respuesta del mensaje, una rutina, que será ejecutada y su valor retornado será la respuesta del mensaje, o bien nothing. Un ejemplo de localidad que incluye un poco todo esto:

Objecto Habitacion_Octogonal "Habitación Octogonal"
with ...
   al_ne "¡La salida noroeste está bloqueada por un muro invisible!",
   al_o  Patio,
   al_e [;
     if (amuleto has puesto)
     {  print "Un trozo del muro este súbitamente se abre
               ante tí, permitiéndote pasar a...^";
        return TumbaOculta;
     }
   ],
   al_s [;
     if (random(5)~=1) return Portico;
     print "El suelo cede inesperadamente, y caes a través de un
            agujero en el yeso...^";
     return random(Laberinto1, Laberinto2, Laberinto3, Laberinto4);
   ];

Hay dos variables que resultan muy útiles cuando se programan rutinas para el manejo de mensajes: self y sender. La variable self siempre contiene al objeto que recibe el mensaje, mientras que sender tiene el valor del objeto que lo ha enviado, o el valor nothing si no ha sido enviado desde un Objeto (sino desde una rutina no embebida). Por ejemplo;

  numero_clave [;
     if (~~(sender ofclass Maxima_Autoridad))
         "Lo siento, no tiene permiso para saber esto.";
     return 16339;
  ];

Acceso a los valores de la superclase

(!) Una situación bastante común al programar en Inform es que tengamos una clase general de objetos (por ejemplo Tesoro), y queremos crear una instancia de esa clase que se comporte ligeramente diferente. Por ejemplo, podríamos tener

Class Tesoro
 with deposito [;
        if (self provides puntos_de_deposito)
          puntuacion=puntuacion+self.puntos_de_deposito;
        else puntuacion=puntuacion+5;
        "Sientes como crece tu autoestima.";
      ];  

y queremos crear una instancia llamada Idolo_Murcielago que, por ejemplo, se va volando cuando intentamos depositarlo, si la localidad está a oscuras. Sería así:

Tesoro Idolo_Murcielago "estatuilla de un murciélago"
 with  deposito [;
         if (localizacion==laoscuridad)
         {
             remove self;
             "¡Se oye un aleteo metálico!";
         }
         ...
       ];

En el lugar de los puntos ... del listado anterior, tendríamos que copiar el código que teníamos antes sobre depósito de tesoros (el de la clase Tesoro) de modo que, si no está a oscuras, se comporte como otro tesoro normal. Queda chapucero el volver a repetir aquí el código, lo que realmente necesitamos es una forma de enviar el mensaje deposito al Idolo_Murcielago, pero "como si no hubieramos cambiado la propiedad deposito heredada de Tesoro". Esto se consigue con el llamado "operador superclase", que se escribe :: (el carácter dos puntos, repetido). El término "superclase" se ha tomado de Smalltalk-80, donde está definido de una forma más ajustada). Por tanto, en lugar de los puntos ..., basta poner:

   self.Tesoro::deposito();

para enviar el mensaje deposito otra vez al murcielago, pero de forma que sea manejado no por la rutina deposito del murcielago, sino por la rutina deposito de la clase Tesoro.

El operador :: también funciona para obtener y cambiar valores de propiedades, y no sólo para el envío de mensajes. En general, al escribir

    objeto.clase::propiedad

nos estamos refiriendo al valor de la propiedad que ha sido declarado en la clase (nos daría un error si la clase no proporciona esa propiedad, o si el objeto no es un miembro de esa clase). Observar que el operador :: es un operador más, que puede formar parte de otras expresiones y asignaciones, por lo que es legal poner:

  x = Tesoro::deposito;
  Idolo_Murcielago.x();

para enviar un mensaje al murcielago, que sea manejado por la rutina originalmente proporcionada por la clase Tesoro. Continuando con el tema de la volatería, el AveDePresa podría tener su propia rutina fuerza_sustentacion:

  fuerza_sustentacion [;
     return self.Bird::fuerza_sustentacion() + self.personas_tragadas;
  ],    

para reflejar la idea de que, a diferencia de otros pájaros, éste aumenta su fuerza comiendo personas.

Filosofía

(!)(!) Esta sección puedes saltártela, al menos hasta que tengas perfectamente claros todos los conceptos que se tratan en este capítulo "Inform como lenguaje de programación". Básicamente está dirigido a aquellos que se preocupan por la solidez todas estas ideas que hay detrás del complicado sistema de Objetos y Clases. Es decir, ¿merece la pena esta complicación? (Como dijo una vez Stephen Fry, "El socialismo está muy bien en la práctica, pero ¿funciona en la teoría?").

Comencemos con un par de definiciones:

objeto
Es un miembro del árbol de objetos del programa, o bien una rutina del programa, o bien una cadena literal (string) del programa. (Por supuesto, las rutinas y los strings no pueden moverse por el árbol de objetos, pero en lo demás si son objetos porque se les puede aplicar las condiciones ofclass y provides, y se les puede enviar mensajes). Los objetos son parte del programa compilado producido por Inform.
clase
Es el nombre abstracto de un conjunto de objetos del juego, que se asocian con un conjunto de características que comparten estos objetos. Las clases en sí mismas, se describen mediante texto en el código fuente, pero no son parte del programa compilado producido por Inform.

He aquí todas las reglas:

  1. Los programas compilados se componen de objetos, los cuales pueden tener enganchadas variables llamadas "propiedades".
  2. El código fuente contiene definiciones tanto de objetos como de clases.
  3. Cualquier objeto dado del programa, o bien es miembro o bien no lo es de cualquier clase dada.
  4. Para cada definición de objeto que aparezca en el código fuente (es decir, para cada directiva Object), se creará un objeto en el programa final. La definición especifica de qué clases es miembro el objeto.
  5. Si un objeto X es miembro de una clase C, entonces X "hereda" los valores de las propiedades que se han especificado en la definición de la clase C

    Los detalles de cómo tiene lugar la herencia se omiten aquí. Pero hay que hacer notar que una de las cosas que pueden ser heredadas de la clase C es el hecho de ser miembro de alguna otra clase, D.

  6. Para cada definición de clase (es decir, para cada directiva Class que aparezca en el código fuente), se crea un objeto en el programa final, llamado "objeto-clase", que la representa.

    Por ejemplo, suponer que tenemos una definición de clase como:

    Class Enano
     with color_barba;
    

    La clase Enano generará un objeto-clase en el programa final, llamado tambíen Enano. Este objeto-clase existe sólo para recibir mensajes como create (crear) y destroy (destruir), y también, de forma más filosófica, para representar el concepto de "enanez" dentro del mundo del juego.

    Es importante recordar que el objeto-clase de una clase, normalmente no es miembro de esa clase. El concepto de enanez, no es un enano, por lo que la condición Enano ofclass Enano es falsa. Un enano concreto proporciona la propiedad color_barba, pero el objeto-clase Enano no proporciona esta propiedad. El concepto de enanez no tiene barba, por lo que la condición Enano provides color_barba sería también falsa.

  7. Las clases que están ya predefinidas por Inform se llaman "metaclases". Hay cuatro: Class, Object, Routine y String.

    De la regla * se deduce que cada programa Inform contiene un objeto-clase para cada una de las metaclases, llamados asimismo Class, Object, Routine y String.

  8. Cada objeto es miembro de una y solo una metaclase.
    1. Los objetos-clase, son miembros de Class, y de ninguna otra clase.
    2. Las rutinas del programa (incluso las que están embebidas en los objetos) son miembros de Rutina, y de ninguna otra clase
    3. Las cadenas de texto estáticas del programa (esto es, todo lo que aparece en el código fuente escrito entre "comillas dobles", incluso si es como parte de la propiedad de un objeto), son miembros de String y de ninguna otra clase.
    4. Los objetos definidos en el código fuente, son miembros de la clase Object, y posiblemente de otras clases definidas en el código fuente.

      Se deduce de la regla * que Class es la única clase cuyo objetos-clase es uno de sus propios miembros (recuerda el ejemplo del enano: la "enanez" no es un enano, pero la "clasez" es una clase). Es decir, la condición Class ofclass Class es cierta, mientras que X ofclass X es falso para cualquier otra clase X.

      Hay otra característica inusual de las metaclaeses, y es la siguiente regla que se proporciona por cuestiones prácticas, aunque no es muy elegante

  9. Contradiciendo las reglas **, los objetos-clase que representan a las cuatro metaclases, no son miembros de Class

Esto termina la lista de reglas. Para ver lo que implican, necesitamos conocer la definición de las cuatro metaclases. Estas definiciones no están escritas explícitamente en ningún lugar dentro de Inform, pero podemos escribir aquí una representación textual de lo que realmente hacen estas metaclases. No existe la directiva Metaclass para definir metaclases, puesto que el programador no puede definirlas, sólo Inform lo hace internamente. Pero para hacer más comprensible la siguiente exposición, supondremos que sí existe esta directiva:

Metaclass Object;

que definiría, en este ejemplo, la metaclase Object. En esta definición vemos que la metaclase Object es la más sencilla posible. No proporciona ninguna propiedad, por tanto no se heredará nada de ella. De modo que los objetos corrientes descritos en el código fuente con la directiva Object, sólo tendrán las propiedades que se especifiquen en su definición.

La definición de la metaclase Class sería algo así:

Metaclass Class
  with    create    [; ...],
          recreate  [instancia; ...],
          destroy   [instancia; ...],
          copy      [instancia1 instancia2; ...],
          remaining [; ...];

Es decir, los objetos-clase pueden responder sólo a estos cinco mensajes, cuyo cometido se describirá con detalle en el siguiente apartado. No proporcionan ninguna otra propiedad. Así, por ejemplo, al ser Enano una clase, habrá un objeto-clase llamado Enano, que proporcionará las cinco propiedades create, recreate, destroy, copy y remaining, pero no la propiedad color_barba (como ya se ha explicado, esta propiedad la tendrán los objetos derivados de la clase Enano, pero no el objeto-clase que representa a esta clase).

Además, de acuerdo con la regla *, los objetos-clase llamados Class, Object, Routine y String, nos proporcionan ni siquiera las cinco propiedades anteriores. No proporcionan, de heho, ninguna propiedad. La razón es que estas cinco propiedades son para crear y destruir objetos en tiempo de ejecución. Pero Inform es un compilador y no, como Smalltalk-80 u otros lenguajes altamente orientados a objetos, un intérprete. No podemos crear el programa mientras se está ejecutando, y es por esto que no tiene sentido enviar peticiones de creación o destruccion de clases, objetos, rutinas o cadenas. (Podíamos haber dado a estas metaclases unas rutinas de create, etc. para permitir estas peticiones, pero que se limitaran a imprimir algún tipo de error si se intenta. Es más eficiente tener la regla * en vez de esto).

La definición de la metaclase Routine sería así:

Metaclass Routine
  with    call [parametros ...; ...];

de modo que las rutinas solo tienen una propiedad llamada call. En el siguiente apartado veremos cómo usar esto.

Finalmente, la metaclase String sería:

Metaclass String
   with   print [;  print_ret (string) self; ],
          print_to_array [array; ...];

de modo que las cadenas proporcionan dos propiedades print y print_to_array. En el próximo apartado veremos cómo usar esto.

Para aclarar conceptos, aquí hay un fragmento de código Inform que representa lo que ocurre cuando es enviado el mensaje O.M(p1, p2,...):

  if (~~(O provides M)) "Error: O no proporciona M":
  P = O.M;
  switch (metaclass(P))
  {
    nothing, Object, Class: return P;
    Routine:                return P.call(p1, p2, ...);
    String:                 return P.print();
  }

Ya que los mensajes call y print están implementados manualmente dentro de Inform, esto no es una definición circular. El ejemplo está simplificado, se han eliminado detalles de lo que ocurre si P es un array.

Enviando mensajes a las rutinas, strings y clases

(!) Hasta ahora, los ejemplos enviaban mensajes únicamente a objetos "reales" (es decir, los creados en el código fuente, que pertenecen a la metaclase Object). Pero existe la posibilidad lógica de enviar mensajes también a los objetos de las otras tres metaclases. La cuestión es saber si están preparados para recibir algún tipo de mensaje. La respuesta es "si", porque Inform proporciona 8 propiedades para estos objetos, que veremos ahora.

Con una Routine, lo único que se puede hacer es llamarla. Si Explorar es el nombre de una rutina, entonces las dos siguientes expresiones son equivalentes:

  Explorar.call(2, 4);
  Explorar(2, 4);

La primera línea envía el mensaje call(2,4) al objeto Explora (que es un objeto de tipo Routine), y el mensaje significa "ejecuta esta rutina con los parámetros (2,4)". La segunda línea hace lo mismo, con otra sintaxis que nos es más familiar. La primera sintaxis tiene una ventaja sobre la segunda, y es que podemos hacer cosas como:

  x = Explorar;
  x.call(2,4);

La respuesta del mensaje call es el valor retornado por la rutina.

A un String se le pueden enviar dos mensajes diferentes. El primero es print, que existe más que nada porque debe existir desde un punto de vista lógico, pero que no es realmente útil. Así, por ejemplo:

  ("¡Se acerca una estampida de bisontes!").print();

imrime la cadena, seguida de un retorno de carro. La respuesta a este mensaje es true (ó 1, que es lo mismo).

(!)(!) El mensaje print_to_array es más útil. Copia el texto de la cadena en los elementos 2, 3, 4, ... del array tipo byte que hay que pasarle como parámetro (cada elemento, será el código de cada letra de la cadena), y escribe el número total de caracteres como un dato tipo word que ocupa los elementos 0 y 1 del array de bytes. Esto es, si A ha sido declarado previamente como un array tipo byte de longitud suficiente, entonces el código:

  ("Una rosa es una rosa es una rosa").print_to_array(A);    

hará que el texto de la cadena se copie en los elementos A->2, A->3, A->4, ..., A->33, y dejará el valor 32 (el número de letras de la cadena) en A-->0 (observar la doble flecha para indicar un acceso tipo word). Además, la respuesta del mensaje será 32 (el número de letras escritas), lo cual es útil.

(!) La los objetos de la metaclase Class se les puede enviar cinco mensajes diferentes, los cuales se tratan en el apartado siguiente. (Pero hay una excepción, y es que a las cuatro metaclases Class, Object, Routine y String no se les puede enviar ningún tipo de mensaje, a pesar de que estos nombres representen también a objetos-clase que son de la metaclase Class).

Creación y destrucción dinámica de objetos

Un problema fastidioso de todos los sistemas orientados a objeto es que a menudo resulta elegante el hacer crecer las estructuras de datos, simplemente conjurando objetos nuevos de la nada, y añadiéndolos a la estructura que ya estaba creadaa. El problema es que, ya que los recursos del computador no son infinitos, llegará un momento en el que ya no podamos conjurar más objetos. El programa debe ser escrito de forma que tenga en cuenta esta posibilidad, y haga algo frente a ella. Esto plantea bastantes dificultades al programador, ya que las condiciones en las que el programa será ejecutado son difíciles de predecir.

En un juego de aventuras, la creación de objetos es útil para algo como un montón de piedras, por ejemplo. Si el jugador quiere coger más piedras, el juego necesita crear un nuevo objeto por cada piedra que entre en juego.

Inform permite la creación de objetos, pero insiste en que el programador debe especificar de antemano el máximo de recursos que se van a necesitar (por ejemplo, el máximo numero de piedras que se pondrán en juego). Aunque esto es un fastidio, la recompensa es que el programa resultante está garantizado, esto es, se ejecutará correctamente en cualquier máquina (o fallará de la misma forma en todas las máquinas). No depende de la memoria o recursos de la máquina en que se ejecuta.

La forma de operar es esta. Cuando se define una clase, se puede especificar un número N que representa el máximo número de instancias de la clase que el programador va a necesitar que existan simultáneamente. Cuando el programa se ejecute, se pueden crear "instancias" nuevas, hasta este límite, o pueden borrarse instancias que ya no se necesiten. Podemos imaginar que tenemos un montón de instancias y que la creación consiste en tomar una instancia del montón y traerla al juego, y el borrado consiste en devolverla al montón.

Las clases pueden recibir los siguientes cinco mensajes:

  1. remaining() Que envía a la clase la pregunta ¿Cuántas instancias más puedo crear?. La respuesta es un valor numérico.
  2. create() Solicita a la clase la creación de una nueva instancia. La respuesta a este mensaje es el objeto recién creado, o el valor nothing (cero) si no pueden crearse más.
  3. destroy(I) Destruye la instancia I, que tuvo que obtenerse previamente con un create()
  4. recreate(I) Destruye la instancia I y la crea de nuevo. El efecto es que los valores de sus propiedades se reinician.
  5. copy(I,J) Copia los valores de las propiedades del objeto J en el objeto I.

(!) Hay que destacar que los mensajes recreate y copy pueden ser enviados a cualquier instancia, y no sólo a las que se han obtenido vía create. Por ejemplo, imagina que Rama_Dorada es un objeto que hereda de las clases Planta y Tesoro. El código

  Planta.copy(Rama_Dorada, Hiedra);    

copia de la Hiedra los valores de todas las propiedades y atributos definidos en la clase Planta, escribiéndolos en la Rama_Dorada. Las propiedades de la Rama_Dorada no heredadas de la clase Planta no son modificadas. De forma análoga,

  Tesoro.recreate(Rama_Dorada);

sólo reinicia las propiedades relacionadas con Tesoro, dejando las propiedades relacionadas con Planta sin tocar.

A menos que la definición de una clase C se haga de forma especial, el mensaje C.remaining() responderá siempre cero, C.destroy() causará un error y C.create() no está permitido. Esto se debe a que el "número mágico" N para una clase es normalmente 0.

La "forma especial" consiste en poner N entre paréntesis tras el nombre de la clase. Por ejemplo, si la definición de la clase Hoja comienza así:

Class Hoja(100) ...

entonces inicialmente Hoja.remaining() responderá 100, y los 100 primeros mensajes create() tendrán éxito. Mensajes create() posteriores tendrán éxito sólo si entretanto se han destruido algunas hojas. En todo lo demás, Hoja es una clase normal.

(!) A veces puede necesitarse una creación y destrucción de objetos más sofisticada que esto. Por ejemplo, podríamos tener una estructura de datos en la cual cada objeto de la clase A está conectado de alguna forma con cuatro objetos de la clase B. Cada vez que se crea un nuevo objeto de la clase A, es necesario crear cuatro nuevos objetos B para él, y cuando A es destruído, también deben destruirse los cuatro B asociados. Por ejemplo, en un juego de aventuras podemos imaginar que cada Enano creado, debe portar un Hacha.

Cuando un objeto nuevo es creado (o recreado), pero antes de que haya sido "soltado" al programa, se envía al objeto un mensaje create (si el objeto proporciona una propiedad create). Esto da una oportunidad al objeto de hacer una inicialización adecuada. De forma similar, cuando un objeto va a ser destruido, pero antes de que se la haya destruido realmente, se le envía un mensaje destroy (si proporciona la propiedad destroy). Por ejemplo:

Class Hacha(30);
Class Enano(7)
 with color_barba,
      create [x;
         self.color_barba=random("negra", "roja", "blanca", "gris");
         ! Y le damos un hacha, si hay alguna disponible
         x=Hacha.create();
         if (x~=nothing) move x to self;
      ],
      destroy [x;
          ! Destruimos todas las hachas que lleve el enano
          objectloop (x in self && x ofclass Hacha)
             Hacha.destroy(x);
      ];    

Nota sobre propiedades comunes frente a individuales

(!)(!) Las propiedades usadas en las secciones anteriores eran todas ellas ejemplos de "propiedades individuales", que son propiedades que algunos objetos pueden proporcionar, y otros no. Existen también las llamadas "propiedades comunes", las cuales son propiedades de la clase Object. Puesto que todos los objetos heredan de esta clase, todos los objetos del juego tienen estas propiedades. Un ejemplo (de la librería InformATE) es capacidad. Podemos leer el valor de capacidad de cualquier objeto del juego, incluso si la definición de este objeto no declara esta propiedad. El valor que obtendríamos en este caso sería 100, ya que este es el valor "por defecto" de esta propiedad (el valor que hereda de Object).

Pero las propiedades comunes tienen un tipo de herencia más débil que las otras. Si un objeto (por ejemplo jaula) no declara expresamente la propiedad capacidad, heredará un valor por defecto para ella, el cual puede ser leído (jaula.capacidad vale 100). Pero no puede ser modificado (jaula.capacidad=5 sería un error). Además la condición provides daría como resultado false (jaula provides capacidad es falso).

Todas las propiedades definidas por la librería InformATE son "propiedades comunes". La razón es que este tipo de propiedades son ligeramente más rápidas de manejar y ocupan ligeramente menos memoria. Pero sólo hay 62 propiedades comunes disponibles, y la librería InformATE ya usa 55 por si sola. Las propiedades individuales, por otra parte, son prácticamente ilimitadas. Por tanto sólo merece la pena crear una propiedad como común si realmente va a ser muy utilizada en tu programa. Para declarar propiedades comunes se usa la directiva:


Property <nombre>;

lo cual debe hacerse después de incluir el fichero EParser, pero antes de usar esa propiedad por primera vez. Esto hará que todos los objetos hereden el valor 0 para esta propiedad. Este es el "valor por defecto" que, opcionalmente, puede ser especificado en la directiva Property. Por ejemplo, InformATE contiene una línea con la declaración

Property capacidad 100;  

que es la causa por la cual todos los recipientes de un juego que no especifiquen otro valor, pueden contener hasta 100 cosas.

Cuando un objeto especifica una propiedad común, por ejemplo:

Object caja "caja"
  with ...
       capacidad 5,
      ...
;  

entonces esa propiedad ya es accesible para ser modificada, y provides evalúa a true, como si fuera una propiedad individual. Si el objeto especifica la propiedad, pero sin darle un valor inicial, éste será de cero. (en lugar de heredar el valor "por defecto").


Zak McKraken - spinf@geocities.com

Uso del compiladorInform como lenguaje de programaciónSiete estructuras de datos especialesEl lenguaje de los Objetos
1