POOI - Intérprete de un sistema orientado a objetos y basado en prototipos

Referencias

Introducción

POOI es un intérprete de un sistema de objetos basados en prototipos, con fines didácticos. Soporta un lenguaje de programación propio muy sencillo. Los objetos se envían mensajes entre sí para obtener un programa que resuelva un problema. Todo es un objeto, incluyendo los datos como números o cadenas de caracteres. La sintaxis de un mensaje es como sigue:

<objeto> <mensaje> [ <param1> [ <param2> [...] ] ]

Por ejemplo, para ver todos los miembros (atributos, es decir, datos) y métodos (las funciones que responderán a los mensajes) en el objeto padre, Object, se podría hacer como sigue: Object list Visualizando en el la ventana de salida la información requerida. Object es el objeto que en última instancia es el padre de todos los objetos, el tope de la jerarquía de herencia de objetos. Así, en Object se encuentran precisamente todos los métodos soportados de manera universal por todos los objetos del sistema, como por ejemplo, list y toString.

La diferencia entre estos dos posibles mensajes a enviar a un objeto es que el primero devuelve una lista exhaustiva de todos los miembros, sean atributos o métodos, mientras que el segundo sólo muestra los contenidos de los atributos.

Manejo básico del intérprete

POOI es capaz de manejar datos de las siguientes clases:
Así, es posible realizar calculos sencillos desde el propio intérprete, como las siguientes (se ha incorporado un símbolo '>' para distinguir lo que introduce el usuario de lo que muestra el intérprete).

> 5 + 6
11
> 6 * 5
30

Para obtener la lista de mensajes que soportan los números enteros (igual a la de los números reales), se puede lanzar el mensaje:

Int list

De la misma forma, para los números reales:

Real list

...y para las cadenas de caracteres:

Str list

Los mensajes no precisan de ser simples, pueden ser complejos, por el simple método de anidar mensajes, encerrándolos entre paréntesis:

(5 * 6) + 2

Primero se multiplica 5 por 6 y a continuación, se le suma un dos. El mensaje se interpreta de izquierda a derecha, sin aplicación de ningún tipo de precedencia al margen de la impuesta por los paréntesis:

(5 * 6) + (4 / 2)

Así, este código obtendría el mismo resultado que el anterior.

Lo que sucede es que el intérprete asume el primer término como el origen del mensaje, mientras que a continuación se indica el mensaje a enviar, seguido de sus parámetros. En la orden "Int list" se listan todos los miembros del objeto Int, ya que eso es lo que hace el método list para él. Cuando se ejecuta la orden 5 * 2, se crearán los objetos cinco y dos como dos objetos que derivan de Int. Al objeto entero con valor 5 se le pasa el mensaje multiplicar (cuyo nombre es "*"), y que conlleva un solo parámetro que es otro objeto entero con valor 2.

Creación de objetos

Los objetos se crean mediante copia. Así, por ejemplo, para crear un nuevo objeto, se utiliza la orden:

> anObject copy
anObject was copied as anObject4

Estos provocará que se creará un nuevo objeto, copia de anObject. Dado que anObject está vació, el nuevo objeto estará vacío. Si se desea darle otro nombre, se puede utilizar el mensaje rename, que acepta como parámetro una cadena que será el nuevo nombre del objeto.

> anObject4 rename "obj"
anObject4 was renamed as obj

Tal y como se ha visto anteriormente, es posible combinar las dos órdenes en una sola, más compleja. Así:

> (anObject copy) rename "obj"
anObject was copied as anObject4
anObject4 was renamed as obj

Para poder confirmar qué miembros tiene el nuevo objeto, es posible utilizar list y str. Ambos mensajes devuelven una representación del objeto. Mientras que el primero lista todos los miembros sin distinción, el segundo devuelve una cadena con los valores de todos los atributos.

> obj str
{ }
> obj list
Object obj = {
    parent = Object Object : {}

}

Añadiendo miembros a un objeto

Para que el objeto sea útil, debe contener información y realizar acciones. Lo primero se logra usando atributos, mientras lo segundo se logra con métodos.

> (anObject copy) rename "Punto"
anObject was copied as anObject4
anObject4 was renamed as Punto

En este ejemplo, se creará un punto con dos coordenadas, x e y. Añadir dos coordenadas al punto es tan simple como sigue:

> Punto.x = 0
> Punto.y = 0

Si se le envía el mensaje str, entonces se podrá comprobar que ahora el objeto tiene dos atributos, dos números enteros con valor cero.

> Punto str
{ x = 0 y = 0 }

El mensaje set es equivalente a "Punto.x = 0", tomando la forma alternativa de "Punto set "x" 0".  Añade un atributo al objeto si este no existe, o lo modifica si existe. Devuelve el propio objeto cambiado, así que se pueden concatenar varios mensajes de este tipo como una sola orden. La notación con el símbolo '=' es un atajo sintáctico.

El comportamiento se puede cambiar creando métodos para que realicen tareas. Por ejemplo, para poder cambiar los atributos, será necesario añadir un método llamado, por ejemplo, cambia. Este método responderá al mensaje del mismo nombre cambiando los atributos x e y.

> Punto.cambia = {a b: (self set "x" a) set "y" = b }
'cambia' set in Root.Punto

Los métodos se delimitan con las llaves: { y }. A continuación de la llave izquierda, se indican los parámetros formales que conllevará. Es decir, cómo se llamarán los valores que serán añadidos cuando el mensaje sea enviado. Los métodos, en su interior, admiten órdenes iguales a las que se han indicado hasta ahora en este documento. Cuando se desee referenciar al objeto que va a atender el mensaje, es necesario utilizar la palabra clave self. Así, el método anterior se traducirá, al ejecutarlo con los valores 10 y 20, en las órdenes 'Punto set "x" 10; Punto set "y" 20', como se puede comprobar a continuación.

> Punto cambia 10 20
'x' set in Root.Punto
'y' set in Root.Punto
> Punto str
{ x = 10 y = 20 }

También será interesante mostrar una salida más elaborada que la que devuelve str. Por ejemplo, se pueden indicar ambas coordenadas separadas por una coma:

> Punto.str = {: ((self.x str) + ", ") + (self.y str) }
'str' set in Punto
> Punto str
10, 20

Se ha ejecutado entonces el método str en el objeto Punto. Para entender completamente esto, es necesario comprender a su vez el modelo de objetos que sigue Pooi.

El modelo de orientación a objetos basado en prototipos

Los sistemas orientados a objetos más típicos, como por ejemplo los lenguajes de programación C++ o Java, se basan en clases. Las clases son los moldes de los que se crean los objetos, de manera que la estructura de los mismos (los miembros que contienen) es exactamente la misma entre ellos. En estos sistemas, la herencia disponible es la herencia por concatenación, de manera que un objeto es el resultado de concatenar los atributos de todas las superclases de las que hereda su clase.

En el modelo de orientación a objetos que sigue Pooi, no existen las clases. Sencillamente, los objetos se copian unos de otros, y se les pueden añadir y eliminar miembros. Algunos objetos, que no se distinguen en nada de los demás son usados como referencias, es decir, son usados para crear los nuevos objetos por copia. Estos objetos de referencia se denominan prototipos, ya que los nuevos objetos pueden ser bastante parecidos a ellos, pero no necesariamente iguales.  La herencia disponible en sistemas orientados a objetos no existe en este modelo de objetos, sino que se sustituye por la herencia por delegación.

Herencia
        por delegación

En el esquema anterior, se puede ver explicado el recorrido para ejecutar un mensaje, siguiendo el modelo de herencia basada en delegación. El objeto objB recibe un mensaje f. Sin embargo, objB no contiene el miembro f, por lo que delega la ejecución del método en su padre. El objeto sabe cuál es su padre porque existe un atributo especial, llamado parent, que le apunta. El objeto B tampoco tiene un miembro f, así que de nuevo se sigue el atributo parent para llegar al objeto A. Este objeto sí tiene un miembro f, por lo que se ejecuta el método f dentro del objeto A, con objB como objetivo u objeto que está ejecutando el método.

> Punto list
Object Punto = {
    str = {: ( self.x str ); ( __POP + ,  ); ( self.y str ); ( __POP + __POP );  }
    cambia = {a b: ( self set x a ); ( __POP set y b );  }
    x = Int x : {}
        10
    parent = Object Object : {}
    y = Int y : {}
        20
}

En el ejemplo, un objeto Punto, que será utilizado como prototipo, contiene dos métodos y dos atributos. Para crer una instancia de Punto, será necesario copiarlo.

> (Punto copy) rename "p1"
Punto was copied as Punto1
Punto1 was renamed as p1

Ahora, es posible utilizar p1 como un Punto, y dejar Punto reservado para crear futuros objetos Punto.

> p1 cambia 11 12
x set to 11
y set to 12
> p1 str
11, 12

Sin embargo, para crear p1 ha sido necesario copiar, además de los atributos, los métodos cambia y str. No tiene demasiado sentido copiar estos métodos, cuando probablemente serían iguales para todos los objetos Punto. Además, es un problema de eficiencia el copiar métodos. En el modelo basado en clases, desde luego este problema no existe, los métodos jamás se copian.

La alternativa es dividir el prototipo en estado y comportamiento. Así, tendriamos un objeto de comportamiento, TraitsPunto, y el propio Punto que será copiado. Estos dos objetos estarán relacionados por herencia, de manera que Punto será un objeto derivado de TraitsPunto.

> (anObject copy) rename "TraitsPunto"
'anObject6' renamed as 'TraitsPunto'
>
TraitsPunto.cambia = {a b: (self set "x" a) set "y" b }
'cambia' set in Root.Punto
> (anObject copy) rename "Punto"
'anObject8' renamed as 'Punto'
> Punto.x = 0

> Punto.y = 0
> Punto.parent =  TraitsPunto
> (Punto copy) rename "p1"
'Punto4' renamed as 'p1'
> p1 cambia 100 200
> p1 str
100, 200

El esquema resultante puede observarse justo a continuación.
Herencia con Punto
De esta manera, el modelo basado en prototipos es capaz de representar el modelo basado en clases.