Fecha de revisión: 25/Feb/2005

A pesar de su expresividad, Freya es también un lenguaje preciso y conciso. Siempre que ha sido posible, hemos intentado evitar que el programador teclee innecesariamente, sólo para complacer el gusto estético del diseñador. La concisión es un atributo admirable, sobre todo cuando se tiene que desarrollar aplicaciones con el Bloc de Notas y el compilador de línea de comandos. Naturalmente, un lenguaje como C# parte con ventaja en este sentido, frente a un derivado de Pascal como Freya. Por este motivo, nuestro punto de referencia para las comparaciones ha sido siempre Delphi.

Una de las áreas donde mejor se refleja este esfuerzo es en la declaración e implementación de propiedades. Este artículo trata sobre las peculiaridades de las propiedades en Freya, y cómo hemos resuelto la escritura de aserciones para propiedades. Nos ocuparemos también aquí de las propiedades vectoriales y eventos.

PROPIEDADES: SINTAXIS BASICA

Aunque gran parte de la sintaxis relacionada con las propiedades se ha tomado directamente de Delphi, con los correspondientes retoques necesarios para .NET, hemos tenido que hacer algunas adaptaciones a la sintaxis de declaración de propiedades. Veamos cómo se declarararía en Freya una propiedad de lectura y escritura, de tipo cadena:

// Freya
Control = class(Component)
    ...
public
    property Text: string;
end;

Observe las diferencias respecto a Delphi:

  1. No hace falta declarar explícitamente que la propiedad permite lecturas y escrituras, porque se asume por omisión.
  2. No se han indicado los nombres de los métodos de acceso. Freya utilizará el mismo nombre de la propiedad para implementar más adelante, en la sección implementation for, los métodos de acceso correspondientes.

Este último detalle nos ahorra mucha redundancia. En Delphi, para declarar una propiedad Text, hay que declarar también un campo FText o una función GetText, más un procedimiento SetText: demasiados nombres, aunque todos ellos están relacionados. Además, Delphi obliga también a repetir el tipo de la propiedad cuando declaramos la variable, la función o el procedimiento.

Y hay más problemas. En Delphi, cuando el programador encuentra el método de acceso leyendo el listado, le cuesta trabajo "adivinar" que GetText, por ejemplo, sólo se utiliza para implementar la propiedad Text. De hecho, puede que en otra parte del código, el autor se haya saltado las reglas y haya ejecutado GetText directamente. Veamos, sin embargo, como se implementa el acceso para lectura en Freya:

implementation for Control is

    property Text: string;
    begin
        GetWindowText(Handle, Result);
    end;

    // ...

Aunque el encabezamiento dice property, se ve a las claras que estamos devolviendo un valor, y que por lo tanto, este código debe corresponder a la implementación del acceso para lectura de la correspondiente propiedad. Simétricamente, el acceso para escritura se implementa de esta otra manera:

    property Text(Value: string);
    begin
        SetWindowText(Handle, Value);
    end;

Nuevamente, no hay posibilidad de equivocación. Respetando el estilo Pascal, podemos dar el nombre que queramos al parámetro formal, aunque recomendamos usar siempre el mismo nombre por uniformidad: Value, en este ejemplo.

PROPIEDADES DE SOLO LECTURA

Veamos ahora un ejemplo de propiedad de sólo lectura:

Stack = class[X]
    ...
public
    property Top: X; read;
end;

En este caso, alteramos la declaración de la propiedad añadiendo el modificador read. La implementación de la propiedad no aporta nada nuevo:

implementation for Stack[X] is

    property Top: X;
    begin
        // Olvidemos de momento las pilas vacías
        Result := Items[Count - 1];
    end;

    // ...

No existen propiedades de sólo escritura en Freya. Una propiedad debe simular la semántica de un campo: si asignamos x en la propiedad P, a continuación debemos poder leer x desde P, o una variante equivalente del mismo valor. Cuando Freya trabaja con clases desarrolladas en otros lenguajes que publican propiedades de sólo escritura, traduce éstas como si se tratara de un método de acceso tipo SetXxx.

ASERCIONES

Uno de los objetivos principales de Freya es la incorporación de aserciones al lenguaje, siguiendo las reglas de la Programación por Contrato. En este artículo no vamos a explicar en qué consiste esta metodología, pero sí presentaremos algunas nociones necesarias para comprender las extensiones realizadas a la sintaxis de las propiedades para incluir soporte para aserciones.

El ejemplo clásico de lenguaje con aserciones es Eiffel. En este lenguaje no existen propiedades, por lo que las precondiciones y postcondiciones sólo se aplican a métodos. Este ejemplo utiliza la sintaxis de Freya, pero refleja bastante bien la sintaxis y semántica correspondiente en Eiffel:

// Tanto Pop como la propiedad IsEmpty son públicos
procedure Pop(V: X);
    requires not IsEmpty;

El error más común al intentar comprender en qué consiste la Programación por Contrato es preguntarse cuándo se evalúan las precondiciones. Sin entrar en demasiados detalles, es mejor que considere inicialmente las precondiciones como parte de la documentación de la clase. En el ejemplo anterior, la precondición asociada a la declaración de Pop indica claramente que no debe aplicar este método a una pila vacía. Esto lo debe saber cualquier programador que quiera usar la clase. Por lo tanto, la precondición de un recurso público, sólo debe contener referencias a otros recursos públicos.

// Incorrecto, porque Count es un campo protegido
procedure Pop(V: X);
    requires Count > 0;

Por el contrario, si Pop estuviese declarado en una sección protegida, la precondición podría hacer referencia a otros recursos públicos y protegidos.

El otro tipo importante de aserción en Eiffel es la postcondición:

procedure Push(V: X);
    ensures not IsEmpty, Top = V;

En este caso, Push "promete" a quien lo llame que, cuando termine su ejecución, la pila no estará vacía, y que además, el elemento en el tope de la pila será el mismo parámetro pasado al método. Siguiendo la explicación que comenzamos para las precondiciones, las postcondiciones ayudan al cliente de la clase a comprender el funcionamiento de la misma. Por otra parte, mientras el autor de la clase escribía el código de ésta, la postcondición podía ayudarle a comprobar si su implementación del contrato era correcta.

Las dos postcondiciones mostradas para Push siguen satisfaciendo el mismo criterio que las precondiciones: como IsEmpty y Top son métodos de igual visibilidad que Push, las dos condiciones son "inteligibles" desde el punto de vista del usuario de la clase. Existen, sin embargo, postcondiciones útiles que no cumplirán con este criterio:

¡Recuerde que esto es Freya!
procedure Stack[X].Push(V: X);
begin
    Items[Count] := V;
    Count++;
ensures
    Count = old Count + 1;
end;

Es cierto que esta postcondición utiliza el campo protegido Count, ¡pero observe que se ha escrito al implementar el método, no junto con su declaración! El cliente de la clase nunca sabrá de la existencia de esta postcondición, porque no la comprendería. Por supuesto, el programador que hereda de la clase debe tener conocimiento de la misma. La separación dual del código de una clase en declaración e implementación, que Freya hereda de Pascal, no ayuda mucho en este caso, pero no hay solución visible al problema.

Este ejemplo muestra también el uso de old. Este modificador, utilizado también en Eiffel, sirve para hacer referencia al valor original de un campo, propiedad o función, antes de ejecutar el método correspondiente.

ASERCIONES CON NOMBRE

Una de las áreas en que nos hemos apartado un poco de la sintaxis de Eiffel es en la forma de dar nombre a una aserción. Este es el estilo Eiffel:

// Eiffel
require stack_not_empty: not IsEmpty;

Como ve, el nombre se sitúa antes de la condición, como si se tratase de una etiqueta. El motivo tras los nombres de aserciones es la posibilidad de fácil identificación del fallo. Algo parecido ofrecen casi todos los dialectos de SQL cuando permiten nombrar una restricción (constraint).

El problema de la sintaxis de Eiffel es que no es fácil de analizar con un compilador sencillo. Hablando en jerga técnica, el lenguaje resultante no es LL(1). Usted puede pensar que eso es "problema interno del compilador". Sin embargo, es criterio aceptado casi universalmente que los lenguajes LL(1) son más fáciles de leer y comprender para nosotros, los humanos. Nuestro cambio es situar la opción del nombre detrás de la condición, separada por la palabra clave as:

// Freya
requires not IsEmpty as stack_not_empty;

O alternativamente:

// Freya
requires not IsEmpty as 'La pila no debe estar vacía';

Observe que, como recompensa, ahora podemos asociar también una constante de cadena como nombre de aserción.

NOTA
También habrá notado que, mientras Eiffel utiliza verbos en imperativo o infinitivo (requerir, asegurar), Freya utiliza el verbo en tercera persona ([este método] requiere tal y cual). Por una parte, la lectura se hace más fluida. Además, Delphi utiliza este mismo criterio en otras construcciones sintácticas: uses unidad, la propiedad Tal implements el tipo de interfaz MasCual...

ASERCIONES Y PROPIEDADES

Las propiedades aportan algunas particularidades al mecanismo de aserciones. Como una propiedad va asociada a dos métodos, uno para la lectura y el otro para la escritura, en el caso más general pueden existir dos juegos de aserciones para una propiedad de lectura y escritura. Es fácil comprobar, además, que tiene sentido permitir la combinación de aserciones:

  1. Precondiciones para lectura: no podemos pedir el valor de Top si la pila está vacía.
  2. Postcondiciones para la lectura: pueden referirse al valor de retorno de la propiedad, o al estado del objeto, en general, aunque lo común es que al obtener el valor de una propiedad no afectemos el estado abstracto de la instancia. Como ejemplo del primer tipo de postcondición, la propiedad Caption de un formulario podría asegurarnos un valor distinto de la cadena vacía.
  3. Precondiciones para la escritura: para modificar determinada propiedad, podríamos exigir que la instancia estuviese en determinado "estado". Podríamos exigir condiciones especiales para el valor a asignar.
  4. Postcondiciones para la escritura: la asignación de una propiedad podría causar cambios paralelos en otras propiedades. Podríamos establecer también equivalencias entre el valor asignado y el valor realmente almacenado en la propiedad. Por ejemplo, si una clase guarda el valor de la propiedad Apellidos en mayúsculas, la postcondición de escritura aseguraría que el valor de entrada sea siempre igual que el valor final de la propiedad, ignorando mayúsculas y minúsculas en la comparación.

Por supuesto, el caso más simple es el de las propiedades de sólo lectura. Por ejemplo:

Stack = class[X]
    ...
    property IsEmpty: Boolean; read;
    property Top: X; read;
        require not IsEmpty;
    ...
end;

Si la propiedad es de lectura y escritura, hay que distinguir el método de acceso para el que se establece la aserción mediante una de las cláusulas for read o for write:

property Caption: string;
    require Value <> '' for write;
    ensure Result <> '' for read;

Value es un identificador especial, del mismo tipo que Result, Self o Inherited, excepto que sólo se admite en una aserción vinculada al acceso en lectura de una propiedad. Las aserciones vinculadas a la lectura pueden hacer referencia a Result, y las aserciones vinculadas a las postcondiciones pueden hacer referencia a Value. De todos modos, la implementación de la escritura de la propiedad puede utilizar el nombre de parámetro que le venga en ganas al programador:

property Caption(Titulo: string);
// Hemos llamado Titulo al parámetro
begin
    SetWindowText(Handle, Value);
end;

Naturalmente, podemos tener postcondiciones "ocultas" asociadas a propiedades.

PROPIEDADES VECTORIALES

Freya permite definir propiedades vectoriales, tanto anónimas, como las que C# admite, como con nombres, que no son admitidas por C#, pero sí por VB.NET. No hay mayor misterio en la declaración e implementación de una propiedad vectorial con nombre:

public

    Dictionary = class[K, V]
    public
        property Count: Integer; read;
        property Key[I: Integer]: K;
        // ...
    end;

implementation for Dictionary[K, V] is

    property Key(I: Integer): K;
        // ...
    end;

    property Key(I: Integer; Value: K);
    begin
        // ...
    end;

    // ...

end.

La propiedad Key se utilizaría de esta manera:

for I: Integer := 0 to Dict.Count - 1 do
    Console.WriteLine(Dict.Key[I].ToString());

Para declarar un indizador por omisión, o indizador anónimo al estilo C#, se utiliza el propio nombre de la clase:

public

    Dictionary = class[K, V]
    public
        property Count: Integer; read;
        property Key[I: Integer]: K;
        property Dictionary[Key: K]: V;
        // ...
    end;

implementation for Dictionary[K, V] is

    property Dictionary(Key: K): V;
        // ...
    end;

    property Dictionary(Key: K; Value: V);
    begin
        // ...
    end;

    // ...

end.

La diferencia entre ambos tipos de propiedades vectoriales se hace evidente al usar el indizador por omisión:

procedure ShowEntry(Dict: Dictionary[string, object]; Entry: string);
begin
    Console.WriteLine(Dict[Entry].ToString());
end;

Como ve, los corchetes se aplican directamente al objeto, en este caso, sin mencionar nombre alguno de propiedad. Por supuesto, salvando la sobrecarga permitida por los parámetros, sólo puede haber un indizador por omisión para cada clase.

EVENTOS

Los eventos se declaran en Freya de forma incluso más sencilla:

public
    StackEvent = procedure (Sender: Object; E: EventArgs);

    Stack = class[X]
        // ...
    public
        event Modified: StackEvent;
    end;

Digo que es más sencillo declarar eventos porque no necesitamos implementarlos: ya lo hace el compilador por nosotros. No obstante, al igual que ocurre en todos los lenguajes compatible con .NET, es posible suministrar nuestros propios métodos de acceso. Si quisiéramos tal cosa, no tendríamos que declararlos junto con la clase, sino incluirlos directamente en la sección de implementación:

implementation for Stack[X] is

    event Modified.Add(Value: StackEvent);
    begin
        // ...
    end;

    event Modified.Remove(Value: StackEvent);
    begin
        // ...
    end;

    // ...

Ni Add ni Remove son palabras reservadas, y sólo tienen un significado especial en este contexto. No es posible implementar solamente uno de los dos métodos de acceso: o elegimos la implementación automática, o tenemos que implementar ambos métodos.


Vea también:

Iteradores en Freya Excepciones en Freya
Métodos en Freya Constructores en Freya
Interfaces en Freya