Presentación del sistema de tipos de Cω

 

Fecha: de publicación (01/08/2005)
Autor: Gerardo Contijoch - mazarin9@yahoo.com.ar

 


Antes que nada, ¿qué es Cω?

Cω (o COmega) es una extensión de C#, un lenguaje que le da nuevas características además de soportar todas las que incluía previamente. Cω intenta llenar el espacio que hay entre el sistema de tipos del Framework .NET, SQL y XML. En la actualidad, si trabajamos en .NET, tenemos que utilizar una API para trabajar con XML (System.Xml.dll) y otra para trabajar con SQL (System.Data.dll). Esto ya no es necesario en Cω. Podemos "escribir XML" en el código sin tener que recurrir a strings o a objetos como los que se encuentran en el namespace System.Xml y podemos hacer SELECTs, en el codigo también, sin tener que ejecutar comandos contra la base de datos.

Pero Cω no sólo ofrece eso, también da la posibilidad de trabajar utilizando concurrencia asincrónica, facilitándonos el trabajo con hilos.

Cω ya hace rato que esta entre nosotros. Inicialmente hubo dos extensiones distintas llamadas Polyphonic C# y X# (luego llamada Xen). La primera intentaba hacer de C# un lenguaje un poco más orientado a la programación concurrente y la segunda traía como novedad la integración del modelo de datos de XML al sistema de tipos de C#. Cω es el resultado de la unión de estos dos proyectos.

¿Por qué Cω?

Como dije anteriormente, Cω es un lenguaje experimental. Eso significa que nunca a va ver la luz como lenguaje, por lo menos esa no es la intención. Sin embargo, es interesante empezar a conocerlo ahora ya que es muy probable que las características más importantes de Cω sean incluidas en la version 3.0 de C# (que se esta desarrollando actualmente junto a la version 2.0).

El sistema de tipos de Cω

Luego de la breve introducción al lenguaje, veamos las novedades que presenta su nuevo sistema de tipos. Debido a su integración con XML es necesario poder representar muchas de las estructuras presentes en XML y así poder trabajar con ellas. Por ejemplo, podemos encontrar a las estructuras anónimas que pueden usarse para representar nodos XML (al igual que registros en una base de datos) o los choice types para representar elementos xs:choice, que pueden tomar más de un valor, pero sólo uno a la vez. Hay que prestar atención también a tres caracteres que se utilizan como "modificadores" de tipos. Estos son "*", "+" y "?". El primero es comunmente utilizado para representar la expresión "0 o más veces", el segundo, "1 o más veces" y el tercero, "0 o 1 vez". Como se verá, al igual que en XML, es posible definir que una variable contenga, por ejemplo, 1 o más valores. No voy a explicar más de esto aca ya que cada "modificador" será explicado a su debido tiempo.

Streams y funciones iteradoras

Los streams son un concepto nuevo en Cω. Pueden verse como colecciones ordenadas o secuencias de objetos a los cuales se los accede de uno en uno. Un stream se declarara colocando un '*' luego de un tipo, por ejemplo, "long*" representa un stream de datos de tipo long, mientras que "Mensaje*" representa un stream de objetos de tipo Mensaje.

Para generar un stream podemos usar una función iteradora. Las funciones iteradoras son simplemente funciones que devuelven un stream y guardan un estado. El estado es utilizado para saber que item de la secuencia se debe devolver cuando es invocada.
Un ejemplo puede que aclare la situación:

// Esta funcion devuelve un stream de números enteros
// (sólo devolvera 2 numeros, primero el 15 y luego el 6)
public int* GetInt(){
    yield return 15;
    yield return 6;
}

// Esta funcion devuelve un stream de cadenas (con al menos 4 cadenas)
public string* GetStrings(){
    yield return "Primer cadena"; 
    yield return "Segunda cadena";
    yield return "Tercer cadena";
    //... mas cadenas
    yield return "Ultima cadena";
}

El primer método devuelve la secuencia (15 6), mientras que el segundo método devuelve una secuencia de cadenas, empezando por "Primer cadena" y terminando por "Ultima cadena". Como se ve, una función iteradora puede contener varios "return", a los cuales hay que anteponerles "yield", indicando así que es una secuencia lo que se devuelve. La línea

yield return x;

podría interpretarse como "el próximo elemento en la secuencia es x". Una función iteradora puede devolver un valor usando sólo el "return" clásico, pero en ese caso no podrá usarse "yield return" dentro de esta. O se usa uno o el otro. Esto significa que una función iteradora puede usarse como una funcion clásica, devolviendo siempre un sólo resultado.

Para acceder a los elementos de un stream podemos usar la sentencia "foreach" de siempre:

public static void main(){

    foreach(int val in GetStrings()){
        Console.WriteLine(val);
    }

}

... o también podemos aprovecharnos de una nueva técnica que aparece en Cω:

public static void main(){
    
    GetStrings().{
        Console.WriteLine(it);
    };

}

No es dificil entender este código. Todo lo que se encuentre entre llaves será aplicado a cada elemento del stream. Habrán notado el uso de la palabra "it". "it" no es más ni menos que un argumento implícito pasado al bloque entre llaves que se usa para hacer referencia a cada uno de los elementos de un stream. Es por ello que uno puede acceder a las propiedades (y metodos) de cada elemento que se devuelve sin ningún esfuerzo.

Las funciones iteradoras, a pesar de devolver varios resultados, sólo necesitan ser invocadas una vez, pero el codigo dentro de ellas es ejecutado a medida que es solicitado. Veamos un ejemplo:

// Variable global
public static int condicion = -1;

public static string* GetStrings2(){

    if (condicion < 0){
        yield return "La condición es menor que 0 (primera vez)";
        yield return "La condición es menor que 0 (segunda vez)";
        yield return "La condición es menor que 0 (tercera vez)";
    }else if(condicion == 0){
        yield return "La condición es igual a 0 (primera vez)";
        yield return "La condición es igual a 0 (segunda vez)";
        yield return "La condición es igual a 0 (tercera vez)";
    }else{
        yield return "La condición es mayor a 0 (primera vez)";
        yield return "La condición es mayor a 0 (segunda vez)";
        yield return "La condición es mayor a 0 (tercera vez)";
    }
    yield return "Fin";
    condicion++;
}

public static void main(){

    foreach(string val in GetStrings2()){
        Console.WriteLine(val);
    }

}

El resultado de ejecutar el código anterior es

"La condición es menor que 0 (primera vez)"
"La condición es menor que 0 (segunda vez)"
"La condición es menor que 0 (tercera vez)"
"Fin"

Esto se debe a que la variable "condicion" vale -1 al ejecutar el método. Cuando se ejecuta el método se podría decir que se genera una especie de "mapa de ejecución". En el ejemplo, anterior este mapa sería algo asi:

1 - Evalúo la condición
2 - Como la condición vale -1 los próximos elementos a devolver son
        "La condición es menor que 0 (primera vez)"
        "La condición es menor que 0 (segunda vez)"
        "La condición es menor que 0 (tercera vez)"
3 - El siguiente elemento de la secuencia a devolver es
        "Fin"
4 - Sumo el contador en 1

Por eso es que a pesar de que accedamos varias veces a los elementos del stream, el contador sólo es incrementado una vez. Los valores que se devuelven son preparados (no calculados) desde el primer momento.

Si quisieramos evaluar la condición antes de imprimir el mensaje, tendriamos que reescribir el código para que quede asi:

// Variable global
public static int condicion = -1;

public static string* GetStrings3(){

    if (condicion < 0){
        yield return "La condición es menor que 0";
    }else if(condicion == 0){
        yield return "La condición es igual a 0";
    }else{
        yield return "La condición es mayor a 0";
    }
    // yield return "Fin";
    condicion++;
}

public static void main(){

  // Se imprime el mensaje "La condicion es menor que 0" y se incrementa el contador
  GetStrings3().{ Console.WriteLine(it); };
  // Se imprime el mensaje "La condicion es igual a 0" y se incrementa el contador
  GetStrings3().{ Console.WriteLine(it); };
  // Se imprime el mensaje "La condicion es mayor que 0" y se incrementa el contador
  GetStrings3().{ Console.WriteLine(it); };

}

Si la penúltima línea de GetStrings3() no estuviera comentada, entonces el mensaje "Fin" se imprimiría luego de cada mensaje.

A simple vista parecería que los streams no nos ofrecen ningúna ventaja frente a los arrays o las colecciones, pero no es así. Si devolviéramos un array con una función, tendríamos que haberlo calculado antes, en cambio con los streams no sucede lo mismo. Debido a que accedemos a los elementos uno por uno, es posible calcular el próximo elemento en la secuencia en el momento de necesitarlo y no previamente. El siguiente ejemplo muestra una consecuencia de esto:

public static int* CalcularValores(){
    int x = 0;
    yield return x + 33;
    yield return x - 10;
    throw new System.Exception("Error!!!");
    yield return x;
    yield return 0;
}

public static void main(){

    try{
        CalculaValores().{ Console.WriteLine(it.ToString()); };
    }catch{
        Console.WriteLine("Error esperado.");
    }
}

Esto produciría el siguiente resultado:

    33
    -10
    Error esperado.

Como se ve, los resultados son calculados en el momento. La excepción solo es lanzada cuando tiene que imprimir el tercer elemento de la secuencia y no antes.

Una propiedad de los streams a tener en cuenta es que un stream no puede contener otro stream, sólo puede "extenderlo". Esto significa que si tenemos la secuencia (H o) podremos anexarle otra secuencia como (l a) y obtendremos (H o l a), pero nunca (H o (l a)).

Streams no vacíos

Un stream también puede definirse utilizando el caracter "+" en vez de "*". La única diferencia entre "string*" y "string+" es que el segundo stream debe contener al menos un elemento (es decir, no ser un stream vacío), mientras que el primero puede no contener ningún elemento. Todo lo explicado anteriormente también es aplicable a los streams declarados con el caracter "+".

Tipos anulables u opcionales

Comunmente los lenguajes actuales no permiten asignar un valor null a un tipo de dato por valor, por ejemplo en C# lo siguiente no es posible:

string x = null;

Pero en Cω si lo es. El ejemplo anterior en Cω puede reescribirse de la siguiente manera:

string? x = null;

Agregando el caracter "?" al tipo de dato, hacemos que la variable declarada como ese tipo, sea anulable. Por ejemplo:

string? x;

x = ObtenerValor(); //Puede devolver null

if (x == null){
    Console.WriteLine("Valor Null.");
}else{
    Console.WriteLine(x.Length.ToString());
}

¿Cuántas veces ocurre una NullReferenceException cuando intentamos leer el valor de una propiedad? Pues con un tipo anulable eso se acabo, ahora el valor devuelto es null. Por ejemplo:

using System.Data.SqlClient;
...
...
SqlCommand? comando = null;
string? ct = comando.CommandText; //Devuelve null y no se produce una NullReferenceException

Hay que tener en cuenta que los tipos anulables siempre se inicializan en null.

Tipos no anulables

Como recién se vio, los tipo anulables nos permiten nos permiten escribir algo asi:

string? x = null;

Ahora, suponiendo que Mensaje es un tipo por referencia, lo siguiente es perfectamente válido:

Mensaje mens = null;

Y ahí es donde entran los tipo no anulables. ¿Qué sucede si deseamos que Mensaje no sea nunca null? En Cω podríamos hacer algo así:

Mensaje! mens = new Mensaje(); // Todo OK

mens = null; // No compila!!

Usando tipos no anulables nos podremos ahorrar unos cuantos dolores de cabeza ya que no correremos riesgos en los casos en que no esperamos que un valor sea null.

Choice Types

Una variable de un "tipo elegible" (de ahora en adelante, "choice type", suena mejor) es una variable que puede tener más de un tipo asociado a ella, pero sólo uno a la vez. El tipo de dato de la variable depende del valor que le hayamos asignado. Por ejemplo, si deseamos declarar una variable que pueda ser un número o una fecha, tendríamos que poner lo siguiente:

choice{DateTime; int;} x;

Así, podemos trabajar de este modo con ella:

choice{DateTime; int;} x;
x = 15;
Console.WriteLine(x.ToString()); // Imprime 15
x = DateTime.Now;
Console.WriteLine(x.ToString()); // Imprime 11/10/2005 10:30:55 AM

No hay límite en la cantidad de tipos que podemos asociar a una variable y el orden en que se listan no influye en nada.

Si podemos usar una variable que puede ser de mas de un tipo, entonces es lógico pensar que también podemos pasarla como parámetro en métodos y funciones y que también estos deberían poder devolver un choice type. Veamos otro ejemplo:

public static string FechaONumero(choice{DateTime;int;} valor){

   

    return valor.ToString(); // Devuelve Int32.ToString() o DateTime.ToString(), segun corresponda

}

public static choice{DateTime;int;} DevuelveFechaONumero(string valor){

   

    if(valor == "fecha")
       
        return DateTime.Now;

   

    if(valor == "numero")
       
        return 66;
}

public static void main(){

    Console.WriteLine(FechaONumero(324)); // Imprime el numero 326
   
    Console.WriteLine(FechaONumero(DateTime.Now)); // Imprime la fecha actual

   

    Console.WriteLine(DevuelveFechaONumero("fecha")); // Imprime la fecha actual
   
    Console.WriteLine(DevuelveFechaONumero("numero")); // Imprime 66

   

    choice{DateTime;int;} tmp;
    Console.WriteLine(FechaONumero(tmp)); // Error!!!

}

Hay que prestar atención a la situación que se presenta cuando una función acepta un parámetro de tipo choice y se le pasa un valor de tipo choice sin inicializar. En este caso se produce una excepción ya que el CLR no es capaz de inicializar la variable con el valor correcto debido a que este puede ser tanto 0 (para int) como 01/01/0001 (para DateTime). De todos modos, esto se resuelve sencillamente dandole un valor nosotros a la variable.

Otro detalle a recordar es que no existe la conversión implícita de un choice type a uno de los tipos de datos que puede tomar la variable declarada como choice. Por ejemplo, el siguiente código no terminaría de compilar:

choice{string;int;} IntOString = "una cadena";
MetodoQueAceptaSoloStrings(IntOString);   // Error!!: no se puede convertir de choice{string;int;} a string

Estructuras anónimas

Las estructuras anónimas, también conocidas como tuplas, son estructuras especiales que no requieren un nombre para el tipo que definen, es decir, son anónimas. Tampoco soportan métodos, sólo campos públicos. Estas son el equivalente al elemento xs:sequence en XML. El siguiente código no es posible en C# por ejemplo:

struct{
   
    int valor1; int valor2;
} s = new {valor1 = 44, valor2 = 56};

Ni siquiera es necesario especificar un nombre a los campos de la escructura:

struct{
   
    int;int;
} s = new {44, 56};

Las estructuras anónimas pueden ser utilizadas para representar registros de una base de datos o nodos XML. Una característica de las estructuras anónimas es que sus campos estan ordenados. Así entonces, podríamos accederlos como si fuera un array:

struct{
   
    int;int;
   
    int;int;
} s = new { 44, 56, 99, -15};

Console.WriteLine((s[2] + s[3]).ToString());

Sin embargo, por algun motivo no es posible hacer lo siguiente (no tengo ni idea de porque):

struct{
   
    int;int;
   
    int;int;
} s = new { 44, 56, 99, -15};

for(int i = 0; i<4 ; i++){
    Console.WriteLine(s[i].ToString()); // No se puede usar i para acceder a los miembros de s,
                                        // hay que usar un numero entre 0 y 3
}

Los ejemplos anteriores sólo usan estructuras con campos de tipo int por simplicidad, pero nada nos impide crear estructuras más complejas como las siguientes:

struct{
   
    int; System.Threading.Thread;
} s1;

struct{
    System.Xml.XmlNode nodo;
   
    string;
} s2 = new {nodo = unXml.SelectSingleNode("ROOT/row"), "nodo de prueba"};

struct{
    Button;
    TextBox;
    Label;
} controles;

// Un stream de estructuras!!
struct{
   
    int numero; string texto;
}* unStream;

// Un stream de estructuras inicializado...
struct{
   
    int edad; string nombre;
}* persona = {yield return new {edad=15, nombre="un nombre"};
               
                yield return new {edad=45, nombre="otro nombre"};
            };

Dentro de una estructura anónima es posible definir más de un campo con el mismo nombre. Esto nos permite acceder a esos campos y obtener un stream como respuesta:

struct{
   
    int valor1; int valor1;int valor1;
} o = new { valor1 = 44, valor1 = 56, valor1 = 0};

o.valor1.{Console.WriteLine(it.ToString())};

También es posible definir más de un campo con el mismo nombre y distinto tipo, pero en este caso el resultado de acceder a esos campos es un stream de un nuevo tipo choice formado por los campos que comparten el mismo nombre. Un ejemplo puede dejarlo más claro:

struct{
   
    string texto;
   
    int numero;
   
    double;
    System.Text.StringBuilder texto;
} textosYNumeros;
...
...
choice{string; System.Text.StringBuilder;}* streamTextos = textosYNumeros.texto;

/* Resultado esperado

struct{
    string; System.Text.StringBuilder;
} soloTextos = textosYNumeros.texto;

*/

NOTA: En la documentación oficial del lenguaje no dice esto, sino que se genera una estructura nueva formada por los campos que comparten nombre. Esto se ve reflejado en el codigo comentado arriba. Es decir, lo que esta comentado es lo que, según la documentación, debería ocurrir, pero en realidad lo que no está comentado es lo que ocurre actualmente. Al menos lo hace en mi máquina...

Clases de contenido

Las clases de contenido parecen clases como cualquier otra, salvo por una diferencia: poseen una estructura anónima que define los campos de la clase. Por ejemplo, esta es una clase de contenido:

class Computadora{
   
    struct{
       
        string Procesador;
       
        double MhzProcesador;
       
        string Motherboard;
       
        int cantRAM;
       
        struct{
           
            struct{
               
                string nombrePeriferico;
            } Periferico;
        }* Perifericos;
       
    };

   

    bool CambiarProcesador(string nuevoProcesador, double Mhz){
        ...
        ...
    }
}

Al ser ésta una clase de contenido no tendremos ningún problema para acceder a los miembros de la estructura.

Este tipo de clase puede ser utilizado para representar fragmentos de un documento XML. El ejemplo anterior puede utilizarse para representar lo que en XSD sería:

<element name="Computadora">
  <complexType>
    <sequence>
      <element name="Procesador" type="string"/>
      <element name="MhzProcesador" type="double"/>
      <element name="Motherboard" type="string"/>
      <element name="cantRAM" type="integer"/>
      <element name="Perifericos" minOccurs="0" maxOccurs="unbounded">
        <complexType>
          <sequence minOccurs="0">
            <element name="Periferico"/>
            	<complexType>
            	    <element name="nombrePeriferico" type="string"/>
            	</complexType>
            </element>
          </sequence>
        </complexType>
      </element>
    </sequence>
  </complexType>
</element>

Para crear una variable del tipo Computadora solo tendríamos que hacer lo siguiente:

c = <Computadora>
       
        <Procesador>NombreProcesador</Procesador>
       
        <MhzProcesador>1500</MhzProcesador>
       
        <Motherboard>UnaMother</Motherboard>
       
        <cantRAM>512</cantRAM>
       
        <Perifericos>
    	   
    	    <Periferico>
               
                <nombrePeriferico>Mouse</nombrePeriferico>
           
            </Periferico>
           
            <Periferico>
               
                <nombrePeriferico>Scanner</nombrePeriferico>
           
            </Periferico>
           
            <Periferico>
               
                <nombrePeriferico>Impresora</nombrePeriferico>
           
            </Periferico>
       
        </Perifericos>
   
    </Computadora>

Esto último es posible en Cω y merece una explicación, aunque mucho no hay para decir, es demasiado obvio lo que esto significa.
Hay que prestar atención a dos detalles: primero, que no estamos trabajando con un string sino con un literal XML (incluso el IDE de VS.NET nos provee de Intellisense!!), y segundo, que no es necesario declarar a la variable. El nodo raíz del fragmento XML es el nombre de la clase por lo que no se necesita declarar la variable de ese tipo ya que se lo estamos especificando de ese modo.

Una vez seteada la variable podemos acceder a sus miembros como estamos acostumbrados:

Console.WriteLine(c.Perifericos.Periferico.nombre);

Y esto no termina aca...

Aún queda más por ver acerca de Cω. Esto sólo es una presentación del nuevo sistema de tipos, todabía no fueron explicadas cosas como:

  • Acceso a miembros descendientes: Se vio un poco de esto, pero no fue explicado. ¿No se preguntaron acaso cómo es posible que accedamos a los miembros de una estructura anónima a través de una clase de contenido? La nueva forma de acceder a los miembros decendientes nos permitirá hacer cosas como esta:
  • class Pintura{
       
        struct {
           
            string nombre;
           
            double valor;
           
            struct{
               
                string nombre;
               
                string apellido;
                DateTime fechaNacimiento;
               
                struct{
                   
                    string nombre;
                   
                    string pais;
                }ciudadNacimiento;
            }autor;
        };
    }
    ...
    static void Main() {
        Pintura p = new Pintura();
       
        string* str = p...nombre; // Noten el uso de tres puntos.
        str.{ Console.WriteLine(it) }; // Imprime en pantalla el nombre de la pintura, el nombre del autor y el nombre de la ciudad.
    }
  • Uso de filtros y comodines para el acceso a los miembros: Podemos filtrar a que miembros queremos acceder por su tipo o por una condición, o incluso usando comodines:
  • string n = datos[it.persona_id = 57].nombre; // Buscamos el campo nombre del elemento del stream llamado datos en donde el campo persona_id vale 57.
    
    c.int::* // Expresión para acceder a todos los campos de tipo int de c.
    
    c.int::valor // Expresión para acceder a todos los campos de tipo int de c llamados valor.
    
  • Uso de sentencias SQL dentro del código: En Cω es común ver código como el siguiente:
  • foreach(int x in (select id from clientes))
    {
        ...
    }
    
  • Expresiones XML: De esto ya hablamos un poco, pero aun queda más por ver.
  • Concurrencia asincrónica: Una característica de Cω de la cual no se habló en este artículo debido a que poco tiene que ver con el sistema de tipos.
  • Espero que les haya parecido interesante el artículo y que haya despertado su curiosidad por este nuevo lenguaje. Para más información pueden buscar en los links que se encuentran al final del artículo (mientras, yo veré si me hago un tiempito para hablar más sobre el tema).


    Links relacionados

    Hay muchos lugares de donde sacar información, pero estos fueron los primeros que visité y algunos de los que más información tienen.

  • Sitio oficial de Cω
  • Documentación oficial de Cω
  • An Overview of Cω
  • .NET Undocumented: COmega
  • .NET Undocumented: COmega II

  • ir al índice