sábado, 8 de noviembre de 2008

diálogo sobre comida que tenía mucho sabor a informática ...

El otro día hablando con mi amigo Diego surgió una conversación muy interesante (como la mayoría de las que tenemos). La cosa derivó en un diálogo sobre comida que tenía mucho sabor a informática ...

La cosa es que nos dio por comparar los 3 sistemas operativos de escritorio mayoritarios (Windows, MacOS y Linux) con la cocina y aquí van algunas de las conclusiones a las que llegamos :


Windows: Comida Rápida

Hamburguesa

  • Es fácil de conseguir.
  • Sirve para saciar el apetito, pero tiene carencias nutricionales.
  • El usuario tiene muy poco control sobre la personalización de la comida (con queso, sin queso, con pepinillos, sin pepinillos y poco más).
  • Las condiciones higiénicas y sanitarias de preparación (seguridad) son dudosas.
  • La publicidad sobre el producto raramente refleja el producto real (prometen mucho más de lo que el comprador recibe al adquirirla).
  • Siempre hay que pagar un poco más para obtener ciertos extras (bebida grande, extra de patatas, etc.).
  • Aunque aparentemente existen muchas variedades de productos, al final, casi todas saben a lo mismo.
  • Las hamburguesas dicen ser de carne, pero no tienes manera de comprobarlo.
  • si algún plato te desagradó, estaba mal preparado y te sentó mal, no te explicarán qué pasó. Te dirán que en el nuevo menú de la carta ya no existe tal problema.
  • Siempre se quejarán de la comida de otros restaurantes y dirán que la suya es mejor, pero ellos hacen las mismas hamburguesas de siempre.
  • Los dueños de los restaurantes de comida rápida dicen ser filántropos, pero jamás regalarán su producto a menos que tengan un beneficio comercial en tal acción.

Mac: Comida Gourmet

Dulce de pitiminí

  • Es cara.
  • La presentación es casi más importante que el producto en sí.
  • El usuario no puede aderezar libremente la comida. Tiene que comerla como se la sirven y si cuestiona el sabor, generalmente se le tachará de tener mal gusto.
  • Mucha gente la come no porque crea que es más sabrosa, sino porque al hacerlo aparenta ser un entendido culinario frente a sus amigos.
  • Sólo la consigues en lugares exclusivos, donde cosas comunes que podrías conseguir en otros lados salen muchísimo más caras.
  • Siempre innovando y reinventando, aunque a veces no por agradar al usuario, sino por mantener vivo el negocio.
  • Te aseguran que está elaborado con lo mejor de lo mejor, pero no te dejan entrar a la cocina a ver cómo lo hacen.
  • Si algún plato te desagradó, estaba mal preparado o te sentó mal, no te explicarán qué pasó, aunque te regalarán un descuento para tu próxima comida.
  • Para ellos, la comida que se hace en otros lados es de mala calidad, aunque no la hayan probado nunca o alguna vez usen sus recetas en sus propios platos.
  • El Gourmet se acercará como una persona sencilla, cercana y espontánea, pero apenas empiece a hablar de sus cosas, te darás cuenta que está en un nivel que no te puedes permitir. Sencillamente lo hace por que es bueno para la imagen de su negocio.

Linux: La Comida Casera

Pollo con tallarines

  • Siempre estuvo ahí, no se inventó por negocio, sino por necesidad y por placer.
  • Se ajusta a todos los bolsillos, puede ser barata, cara o incluso gratuita.
  • Se cuida la presentación, pero lo importante es la comida.
  • Puedes compartir las recetas con tus amigos. No hay secretos.
  • La puedes hacer tú, un amigo, o puedes recurrir a restaurantes de comida casera, hasta te la pueden llevar a casa o hacértela por encargo.
  • A veces se te puede quemar, pero a la próxima ya sabes cómo evitarlo y te saldrá cada vez mejor.
  • Puede ser jugosa a la vez que muy nutritiva, y la puedes combinar con otros platos.
  • Si la compartes con alguien que la necesita, no vendrá un empleado a decir que tu invitado que no paga no tiene derecho a estar en el restaurante.
  • No hace falta sacarle fotos y disfrazarla para que se vea apetitosa. Lo que ves es lo que obtienes.
  • Para toda la gente, para todas las ocasiones. Hay recetas sencillas y elaboradas, platos para matar el “gusanillo” y para “hincharte a comer”.
  • Existe una variedad inmensa de recetas. Y las puedes usar y modificar a tu antojo.
  • Los demás cocineros son gente como tú, puedes hablar cara a cara con ellos, sencillamente por que les gusta lo que hacen, les encanta la cocina.
  • Aunque por la calle y en la televisión se ve mucha gente yendo a restaurantes de comida rápida y de comida gourmet, hay mucha más gente de la que imaginas que cocina en casa.
  • Los utensilios y productos que necesitarás para prepararla están disponibles en cualquier tienda, o incluso puedes utilizar utensilios caseros y antiguos que tengas en casa.
  • Una vez que la tienes servida en tu plato, lista para comerla, te darás cuenta que nada de lo que puedas comprar por ahí se compara con tu propia comida casera, preparada a tu gusto.
--> Leer más...

miércoles, 5 de noviembre de 2008

Python

Python
Paradigma: multiparadigma
Apareció en: 1990
Diseñado por: Guido van Rossum
Última versión: 2.6 (octubre 2008)
Tipo de dato: fuerte, dinámico
Implementaciones: CPython, Jython, IronPython, PyPy
Influido por: ABC, TCL, Perl, Modula-3, Smalltalk
Ha influido: Ruby, Boo

Python es un lenguaje de programación interpretado creado por Guido van Rossum en el año 1990.[1]

Se compara habitualmente con TCL, Perl, Scheme, Java y Ruby. En la actualidad Python se desarrolla como un proyecto de código abierto, administrado por la Python Software Foundation. La última versión estable del lenguaje es [1] la 2.6 (01 de octubre de 2008) (Se anunció la llegada de la versión 3.0 para agosto de 2008, aunque fue el 17 de septiembre de dicho año cuando se lanzó la primera versión rc1 -release candidate- de dicha versión).

Python es considerado como la "oposición leal" a Perl, lenguaje con el cual mantiene una rivalidad amistosa. Los usuarios de Python consideran a éste mucho más limpio y elegante para programar.

Python permite dividir el programa en módulos reutilizables desde otros programas Python. Viene con una gran colección de módulos estándar que se pueden utilizar como base de los programas (o como ejemplos para empezar a aprender Python). También hay módulos incluidos que proporcionan E/S de ficheros, llamadas al sistema, sockets y hasta interfaces a GUI (interfaz gráfica con el usuario) como Tk, GTK, Qt entre otros...

Python es un lenguaje de programación interpretado, lo que ahorra un tiempo considerable en el desarrollo del programa, pues no es necesario compilar ni enlazar. El intérprete se puede utilizar de modo interactivo, lo que facilita experimentar con características del lenguaje, escribir programas desechables o probar funciones durante el desarrollo del programa. También es una calculadora muy útil.

El nombre del lenguaje proviene de la afición de su creador original, Guido van Rossum, por los humoristas británicos Monty Python.[2] El principal objetivo que persigue este lenguaje es la facilidad, tanto de lectura, como de diseño.

Contenido

Historia

Python fue creado a finales de los ochenta[3] por Guido van Rossum en CWI en los Países Bajos como un sucesor del lenguaje de programación ABC, capaz de manejar excepciones e interactuar con el sistema operativo Amoeba[4] .

Van Rossum es el principal autor de Python, y su continuo rol central en decidir la dirección de Python es reconocido, refiriéndose a él como Benevolente dictador vitalicio o Benevolent Dictator for Life (BDFL).

En 1991, van Rossum publicó el código (versión 0.9.0) en alt.sources. En esta etapa del desarrollo ya estaban presentes clases con herencia, manejo de excepciones, funciones, y los tipos medulares: list, dict, str y así sucesivamente. Además en este lanzamiento inicial aparecía un sistema de módulos adoptado de Modula-3; van Rossum describe el módulo como "uno de las mayores unidades de programación de Python".[3] El modelo de excepciones en Python es parecido al de Modula-3, con la adición de una cláusula else[4] . En el año 1994 se formó comp.lang.python, el foro de discusión principal de Python, marcando un hito en el crecimiento del grupo de usuarios de este lenguaje.

Python alcanzó la versión 1.0 en enero de 1994. Una característica de este lanzamiento fueron las herramientas de la programación funcional: lambda, map, filter y reduce. Van Rossum explicó que "Hace 12 años, Python adquirió lambda, reduce(), filter() and map(), cortesía de un hacker de Lisp que las extrañaba y que envió parches."[5] El donante fue Amrit Prem; no se hace ninguna mención específica de cualquier herencia de Lisp en las notas de lanzamiento.

La última versión liberada proveniente de CWI fue Python 1.2. En 1995, van Rossum continuó su trabajo en Python en la Corporation for National Research Initiatives (CNRI) en Reston, Virginia donde lanzó varias versiones del software.

Durante su estancia en CNRI, van Rossum lanzó la iniciativa Computer Programming for Everybody (CP4E), con el fin de hacer la programación más accesible a más gente, con un nivel de 'alfabetización' básico en lenguajes de programación, similar a la alfabetización básica en inglés y habilidades matemáticas necesarias por muchos trabajadores. Python tuvo un papel crucial en este proceso: debido a su orientación hacia una sintaxis limpia, ya era idóneo, y las metas de CP4E presentaban similitudes con su predecesor, ABC. El proyecto fue patrocinado por DARPA.[6] En el año 2007, el proyecto CP4E está inactivo, y mientras Python intenta ser fácil de aprender y no muy arcano en su sintaxis y semántica, alcanzando a los no-programadores, no es una preocupación activa.[7]

En el año 2000, el principal equipo de desarrolladores de Python se cambió a BeOpen.com para formar el equipo BeOpen PythonLabs. CNRI pidió que la versión 1.6 fuera pública, continuando su desarrollo hasta que el equipo de desarrollo abandonó CNRI; su programa de lanzamiento y el de la versión 2.0 tenían una significativa cantidad de translapo.[8] Python 2.0 fue el primer y único lanzamiento de BeOpen.com. Después que Python 2.0 fuera publicado por BeOpen.com, Guido van Rossum y los otros desarrolladores PythonLabs se unieron en Digital Creations.

Python 2.0 tomó una característica mayor del lenguaje de programación funcional Haskell: list comprehensions. La sintaxis de Python para esta construcción es muy similar a la de Haskell, salvo por la preferencia de los caracteres de puntuación en Haskell, y la preferencia de Python por palabras claves alfabéticas. Python 2.0 introdujo además un sistema de recolección de basura capaz de recolectar referencias cíclicas.[8]

Posterior a este doble lanzamiento, y después que van Rossum dejó CNRI para trabajar con desarrolladores de software comercial, quedó claro que la opción de usar Python con software disponible bajo GPL era muy deseable. La licencia usada entonces, la Python License, incluía una cláusula estipulando que la licencia estaba gobernada por el estado de Virginia por lo que, bajo la óptica de los abogados de Free Software Foundation (FSF), se hacía incompatible con GNU GPL. CNRI y FSF se relacionaron para cambiar la licencia de software libre de Python para hacerla compatible con GPL. En el año 2001, van Rossum fue premiado con FSF Award for the Advancement of Free Software.

Python 1.6.1 es esencialmente el mismo que Python 1.6, con unos pocos arreglos de bugs, y con una nueva licencia compatible con GPL.[9]

Python 2.1 fue un trabajo derivado de Python 1.6.1, así como también de Python 2.0. Su licencia fue renombrada: Python Software Foundation License. Todo el código, documentación y especificaciones añadidas, desde la fecha del lanzamiento de la versión alfa de Python 2.1, tiene como dueño a Python Software Foundation (PSF), una organización sin ánimo de lucro fundada en el año 2001, tomando como modelo la Apache Software Foundation.[9] Incluido en este lanzamiento fue una implementación del scoping más parecida a las reglas de static scoping (del cual Scheme es el originador).[10]

Una innovación mayor en Python 2.2 fue la unificación de los tipos en Python (tipos escritos en C), y clases (tipos escritos en Python) dentro de una jerarquía. Esa unificación logró un modelo de objetos de Python puro y consistente.[11] También fueron agregados los generadores que fueron inspirados por el lenguaje Icon.[12]

Las adiciones a la biblioteca estándar de Python y las decisiones sintácticas fueron influenciadas fuertemente por Java en algunos casos: el package logging,[13] introducido en la versión 2.3,[14] el parser SAX, introducido en 2.0, y la sintaxis del patrón decorator que usa el @,[15] agregado en la versión 2.4[16]

En febrero de 2008, la última versión de producción de Python es la 2.5.2

Características y paradigmas

Python es un lenguaje de programación multiparadigma. Esto significa que más que forzar a los programadores a adoptar un estilo particular de programación, permite varios estilos: programación orientada a objetos, programación estructurada y programación funcional. Otros muchos paradigmas más están soportados mediante el uso de extensiones. Python usa tipo de dato dinámico y reference counting para el manejo de memoria. Una característica importante de Python es la resolución dinámica de nombres, lo que enlaza un método y un nombre de variable durante la ejecución del programa (también llamado ligadura dinámica de métodos).

Otro objetivo del diseño del lenguaje era la facilidad de extensión. Nuevos módulos se pueden escribir fácilmente en C o C++. Python puede utilizarse como un lenguaje de extensión para módulos y aplicaciones que necesitan de una interfaz programable. Aunque el diseño de Python es de alguna manera hostil a la programación funcional tradicional del Lisp, existen bastantes analogías entre Python y los lenguajes minimalistas de la familia del Lisp como puede ser Scheme.

Filosofía

Los usuarios de Python se refieren a menudo a la Filosofía Python que es bastante análoga a la filosofía de Unix. El código que sigue los principios de Python de legibilidad y transparencia se dice que es "pythonico". Contrariamente, el código opaco u ofuscado es bautizado como "no pythonico" ("unpythonic" en inglés). Estos principios fueron famosamente descritos por el desarrollador de Python Tim Peters en El Zen de Python

  1. Bello es mejor que feo.
  2. Explícito es mejor que implícito.
  3. Simple es mejor que complejo.
  4. Complejo es mejor que complicado.
  5. Plano es mejor que anidado.
  6. Ralo es mejor que denso.
  7. La legibilidad cuenta.
  8. Los casos especiales no son tan especiales como para quebrantar las reglas.
    1. Aunque lo práctico gana a la pureza.
  9. Los errores nunca deberían dejarse pasar silenciosamente.
    1. A menos que hayan sido silenciados explícitamente.
  10. Frente a la ambigüedad, rechaza la tentación de adivinar.
  11. Debería haber una -y preferiblemente sólo una- manera obvia de hacerlo.
    1. Aunque esa manera puede no ser obvia al principio a menos que usted sea Holandés.[17]
  12. Ahora es mejor que nunca.
    1. Aunque nunca es a menudo mejor que ya.
  13. Si la implementación es dificil de explicar, es una mala idea.
  14. Si la implementacion es fácil de explicar, puede que sea una buena idea.
  15. Los espacios de nombres (namespaces) son una gran idea ¡Hagamos más de esas cosas!

Desde la versión 2.1.2, Python incluye estos puntos (en su versión original en inglés) como un huevo de pascua que se muestra al ejecutar import this.

Modo interactivo

El intérprete de Python estándar incluye un modo interactivo, en el cual se escriben las instrucciones en una especie de shell: las expresiones pueden ser introducidas una por una, pudiendo verse el resultado de su evaluación inmediatamente. Esto resulta útil tanto para las personas que se están familiarizando con el lenguaje como también para los programadores más avanzados: se pueden probar porciones de código en el modo interactivo antes de integrarlo como parte de un programa.

Existen otros programas, tales como IDLE e IPython, que añaden funcionalidades extra al modo interactivo, como el auto-completar código y el coloreado de la sintaxis del lenguaje.

Ejemplo del modo interactivo:

>>> 1+1
2
>>> a = range(10)
>>> print a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Elementos del lenguaje [editar]

Python fue diseñado para ser leído con facilidad. Entre otras cosas se utilizan palabras en inglés donde otros lenguajes utilizarían símbolos (por ejemplo, los operadores lógicos || y && en Python se escriben or y and, respectivamente).

En vez de delimitar los bloques de código mediante el uso de llaves ({}), Python utiliza la indentación. Esto hace que la misma sea obligatoria, ayudando a la claridad y consistencia del código escrito (incluso entre varios desarrolladores):

Función factorial en C Función factorial en Python
int factorial(int x)
{
if (x == 0)
return 1;
else
return x*factorial(x - 1);
}
def factorial(x):
if x == 0:
return 1
else:
return x * factorial(x-1)

Comentarios

Los comentarios se inician con el símbolo #, y se extienden hasta el final de la línea.

Variables

Las variables se escriben de forma dinámica. El signo igual (=) se usa para asignar valores a las variables:

x = 1
x = 'texto' # esto es posible porque los tipos son asignados dinámicamente

Tipos de datos

Los tipos de datos se pueden resumir en esta tabla:

Tipo Clase Notas Ejemplo
str String Inmutable 'Wikipedia'
unicode String Versión Unicode de str u'Wikipedia'
list Secuencia Mutable, puede contener diversos tipos [4.0, 'string', True]
tuple Secuencia Inmutable (4.0, 'string', True)
set Conjunto Mutable, sin orden, no contiene duplicados set([4.0, 'string', True])
frozenset Conjunto Inmutable, sin orden, no contiene duplicados frozenset([4.0, 'string', True])
dict Mapping Grupo de pares claves, valor {'key1': 1.0, 'key2': False}
int Número entero Precisión fija 42
long Número entero Precisión arbitraria 42L ó 456966786151987643L
float Número Coma flotante 3.1415927
bool Booleano Valor booleano verdadero o falso True


Tuplas y Listas

  • Para declarar una lista, basta usar los corchetes([ ]), mientras que para declarar una tupla se deben usar los paréntesis ().
  • Tanto las listas como las tuplas pueden contener elementos de diferentes tipos.
  • Para acceder a los elementos de una lista o tupla, se utiliza un índice entero. Se pueden utilizar índices negativos para acceder elementos a partir del final del array.
  • Las listas se caracterizan por ser mutables, es decir, se puede cambiar su contenido, mientras que no es posible modificar el contenido de una tupla ya creada, puesto que es inmutable.
>>> lista = ['aaa', 1, 90]
>>> lista[-1]
90
>>> lista[0] = 'aaa'
>>> lista[0:2]
['aaa', 1]

>>> tupla = (1, 2, 3)
>>> tupla[0] = 2
( genera un error )
>>> tupla[0]
1
>>> otratupla = (tupla, ('a', 'b')) # es posible anidar tuplas

Diccionarios

Los diccionarios se declaran entre llaves ({}), la que puede contener pares de valores separados por dos puntos (:).

En este ejemplo, se recorre la estructura de diccionario, y se imprime su contenido de la forma clave=valor

>>> dict = {"dia": 24, "mes": "agosto"}
>>> for k in dict:
... print "%s=%s" % (k, dict[k])

dia=24
mes=agosto

Conjuntos (Sets)

Los conjuntos se declaran mediante la instrucción set. Los conjuntos no tienen orden ni permiten tener elementos repetidos.

>>> conj = set(['a','b','a'])
>>> print conj
set('a','b')

Funciones

Las funciones se definen con la palabra clave def, seguida del nombre de la función y sus parámetros. Otra forma de crear funciones, aunque más limitada, es con la palabra clave lambda (que aparece en lenguajes funcionales como Lisp).

>>> f = lambda x: x+2
>>> f(4)
6

Sistema de objetos

En Python, todo es un objeto (incluso las clases). Las clases, al ser objetos, son instancias de una metaclase. Python además soporta herencia múltiple y polimorfismo.

Biblioteca estándar


Python viene con "baterías incluidas"

Python tiene una gran librería estándar, usada para una diversidad de tareas. Esto viene de la filosofía "baterías incluidas" ("batteries included") para módulos de Python. Los módulos de la biblioteca estándar pueden ser mejorados por módulos personalizados escritos tanto en C o en Python. Debido a la gran variedad de herramientas incluidas en la biblioteca estándar combinada con la habilidad de usar lenguajes de bajo nivel como C y C++, los cuales son capaces de interactuar con otras bibliotecas, Python es un lenguaje que combina su clara sintaxis con el inmenso poder de lenguajes menos elegantes.

Implementaciones

Python posee diversas implementaciones:

Licencias

Python posee una licencia de código abierto, denominada Python Software Foundation License, que es compatible con la licencia GPL. Esta licencia no obliga a liberar el código fuente al distribuir los archivos binarios.[18]

Ejemplos de código Python

El siguiente ejemplo muestra cómo obtener una lista con los cuadrados de los números del 0 al 9 (inclusive).

print [x**2 for x in range(10)]

El siguiente ejemplo saludará al usuario si se conoce su plato preferido, o le preguntará cuál es, en caso contrario.

# -*- coding: iso8859-15 -*-
# diccionario de comidas preferidas de cada persona
comidas = {
"Juan" : "tiburón en escabeche",
"Pablo" : "paella don Beto",
"Alfredo" : "sesos de mono",
"Fulano" : "cucarachas fritas",
"Sonia" : "burros",
"Lucas" : "hamburguesas Mr Paul",
"Jose" : "ceviche",
"Cesar" : "arroz con pollo"
}
afirmativas = set(['si','s','mucho','porfa'])

def saluda(nombre, comida):
respuesta = raw_input("Hola " + nombre + ", ¿te gusta " + comida + "? ")
if respuesta not in afirmativas:
pregunta(nombre)

def pregunta(nombre):
respuesta = raw_input("Hola " + nombre + ", ¿qué comida te gusta? ")
comidas[nombre] = respuesta

for nombre in ["Juan", "Pablo", "Alfredo", "Fulano", "Sonia", "Lucas", "Mengano","Cesar"]:
if nombre in comidas and comidas[nombre] is not None:
saluda(nombre, comidas[nombre])
else:
pregunta(nombre)

El siguiente ejemplo busca y cuenta palabras palíndromas (se leen igual al derecho y al revés) en un archivo de texto que le indiquemos.

ruta = raw_input("Digite la ruta del archivo a leer: ")
archivo = open(ruta, 'r')
texto = archivo.read()

contar = 0

for palabra in texto.split(' '):
if palabra == palabra[::-1]:
contar += 1

if contar == 0:
print "No hay palabras palíndromas en el archivo"
else:
print "Palabras palíndromas encontradas: ", contar

archivo.close()
--> Leer más...

El lenguaje de programación D

En este artículo se hará una introducción al lenguaje de programación D para lo cual se explicarán las principales diferencias con respecto a C y C++; por ello sería aconsejable que el lector de este pequeño artículo conozca las características dichos lenguajes (u otros similares, como Java o C#) y algunos conceptos como la programación orientada a objetos o el control de errores mediante excepciones aunque no es necesario que sea un experto en dichos temas.

Este artículo no es un manual ni un tutorial y por lo tanto no es la mejor referencia para aprender el lenguaje, aunque alguien que conozca C++ o de Java podrá tener una idea muy completa de D tras leerlo.

En mi opinión, D es un lenguaje con un potencial increible, capaz de proporcionar al desarrollador la productividad y la elegancia de Python o Ruby pero con la eficiencia de C.

Este documento no está terminado y se irá ampliando con el tiempo; puede verse una lista de las cuestiones pendientes al final del mismo. La versión actual es la: 0.3 actualizada el 18 de septiembre de 2008. El documento es básicamente un resumen de la documentación de D, traducido, y por lo tanto la mayoría de los ejemplos están tomados de la misma. Aconsejo al lector interesado en el lenguaje que tras leer el artículo y haberse hecho una idea general, lea la referencia completa en digitalmars.com/d.

Cambios al artículo:

  • 18 de septiembre: ampliada la sección de cadenas, creada sección de propiedades (diatriba incluida), creada sección de guardas de ámbito. Arreglado "bug" en el ejemplo de los delegados (gracias Miguel.)

¿Qué tipo de lenguaje es D?

La clasificación del lenguaje de programación D sería: lenguaje compilado a código nativo (con compilación opcional a .NET como C#), orientado a objetos pero permitiendo programar con funciones libres y clases ligeras (structs), con plantillas y mixins (que permiten la programación genérica) y con posibilidad de acceso a bajo nivel.

¿Qué diferencias tiene con respecto a C++?

Esta clasificación parece exactamente la misma que la de C++ y no es casual, pues C++ es el lenguaje al que D aspira a sustituir. D es un lenguaje evolucionario, no revolucionario y en un principio puede parecer que no aporta demasiado sobre C++. Pero no son las características generales sino los detalles de las mismas los que marcan la diferencia. En primer lugar D conserva todas las características de expresividad de C++ (cosa que ni C# ni mucho menos Java consiguen en su afán por hacerse lenguajes más accesibles), pero con una sintaxis y unas construcciones mucho más sencillas y lógicas. Además, otro de los puntos fuertes de C++, su rendimiento, también se ve reflejado en D (en algunas ocasiones incluso superado.)

Por otro lado D cuenta con muchas otra características de las que C++ no dispone, de las cuales vamos a hacer un pequeño repaso a continuación. Quisiera recordar al lector que, al contrario de lo que pasa con Java o C#, estas características no suponen una perdida apreciable de rendimiento para D en comparación con C++:

Gestión automática de memoria (recolección de basura)

En C++ cuando creamos objetos que queremos que transciendan el ámbito de la función, método o bloque de código que los ha creado los creamos con utilizando el operador "new". Este operador se encargará de asignar automáticamente la memoria que sea necesaria para el objeto y llamará a su constructor, devolviéndonos una referencia al objeto ya inicializado que podremos almacenar en una variable del tipo del objeto.

Cuando ya no vamos a necesitar el objeto procedemos a borrarlo con el operador "delete" lo que liberará la memoria asignada por el objeto. ¿Parece sencillo verdad? Bien, debería serlo, pero en la práctica es raro el programa en C++ que no tiene o ha tenido una fuga de memoria porque el flujo de ejecución de una parte del mismo termina antes de llegar al delete (y por lo tanto la memoria queda asignada para siempre sin posibilidad de recuperarla) o que ha fallado porque erróneamente se ha hecho un delete del mismo objeto dos veces o se ha intentado acceder (también erróneamente) a un miembro de un objeto previamente borrado. Obviamente estos errores son evitables pero errar es humano y cuantas más tareas delegue en el programador el lenguaje de programación más posibilidad hay de que haya errores (si no fuera así, todos seguiríamos haciendo ensamblador.)

La gestión automática de memoria quiere decir que el programador seguirá creando los nuevos objetos con "new" pero ya no tendrá que preocuparse de borrarlos con "delete" porque existirá un "recolector de basura" que se encargará de eliminar automáticamente los objetos para los que ya no exista ninguna referencia. Es decir, si tenemos la siguiente función:

void pierdeaceite() {
Obj *o = new Obj();
}

En C++ esto produciría una fuga de memoria cada vez que la llamáramos porque al terminar la función ya no habría ninguna referencia a "o" (y por lo tanto no podríamos usarlo) y sin embargo al no haber hecho delete seguiría existiendo en la memoria. El código equivalente en D:

void nopierdeaceite() {
Obj o = new Obj();
}

No produciría ninguna perdida de memoria porque el recolector de basura detectaría que como "o" fue declarado dentro de "nopierdeaceite" y su alcance termina al terminar la función, la memoria puede ser liberada.

Esta característica (con la que cuentan también C# y Java) puede desactivarse si por cuestiones de rendimiento o de cualquier otro tipo no queremos que se ejecute el recolector de basura llamando a gc.disable() y luego reactivarse llamando a gc.enable(). Lo más espectacular es que mientras el recolector de basura sigue desactivado las referencias a los objetos que hagamos con "new" se seguirán contabilizando por lo que una vez que re-activemos la recolección de basura esos objetos serán igualmente gestionados (no necesitaremos llamar a delete sobre los mismos.) Esto no es así en C# ni en Java,por ejemplo.

Finalmente también podemos borrar, como en C++, nosotros mismos los objetos llamando a "delete" sin que ello cause ningún tipo de conflicto con el recolector de basura.

Gestión de errores mediante manejo de excepciones

C++ permite gestionar los errores mediante el mecanismo de manejo de excepciones, aunque por herencia de C en demasiadas ocasiones se sigue utilizando el sistema de "valores mágicos devueltos por la función".

En D el sistema de manejo de excepciones es superior al de C++ al incorporar algunas características de lenguajes más recientes. Cuando una excepción producida en código D no se captura, se muestra un mensaje de error con información de la excepción. La sintáxis es parecida a la de otros lenguajes con manejo de excepciones:

try {

throw new Exception("mensaje de error");
}
catch(Exception e) {
printf("capturada la excepción, mensaje: %.*s\n", e.msg);
} finally {
printf("este mensaje se mostrará siempre");
}

El código que se ejecuta dentro del Try en caso de lanzar una excepción se ejecutará el código que esté en el bloque del "catch" si la excepción especificada entre paréntesis coincide con la lanzada o es padre de la misma en la jerarquía de la excepción (las excepciones son clases con su propia jerarquía.) El padre de la jerarquía es la clase "Exception" así que en ejemplo el catch capturaría cualquier excepción. En el caso del constructor de la excepción en la instrucción que lo lanza (el "throw") hemos especificado una cadena como parámetro pues en la clase Exception el argumento del constructor es un mensaje que queda almacenado en el miembro "msg", que generalmente se va a usar para amplíar la información del error, y ese miembro es el que imprimimos precisamente en el printf del catch.

El bloque con la instrucción "finally" se ejecutará siempre, se produzca una excepción o no y en caso de que se produzca incluso aunque se capture en un catch. Por lo tanto la salida de este programa sería:

capturada la excepción, mensaje: mensaje de error
este mensaje se mostrará siempre

Guardas de ámbito para asegurar la ejecución de código a la salida de un ámbito

Aunque D tenga recolección automática de memoria hay otro tipo de recursos que se siguen teniendo que liberar manualmente. Ejemplos de estos recursos son los ficheros, los cerrojos y mutex; pero hay otros. Normalmente un código como éste (robado de la página de D)

    void mifuncion() {
Mutex cerrojo = new Mutex();
lock(cerrojo); //Adquirimos
funcionpeligrosa(); // Puede lanzar excepciones
unlock(cerrojo); // Liberamos
}

En este ejemplo puede verse fácilmente el problema: si "funcionpeligrosa" lanza una excepción, no se llama al "unlock(cerrojo)" y por lo tanto el mutex queda bloqueado "para siempre". Una posible solución es usar una construcción de tipo try... finally y poner el unlock dentro del finally, relanzando de nuevo la excepción recibida, pero D nos ofrece una solución más escueta y elegante llamada "Scope Guards" ("Guardas de Ámbito".) Los scope guards se implementan llamando a la palabra clave scope poniendo entre paréntesis el ámbito y después una linea o más (con llaves en el segundo caso.) Lo que hace es sencillo: cuando se llegue alcance el ámbito especificado, se ejecutará el código que hubiéramos asociado con el scope. Lo veremos más fácilmente con el ejemplo:

    void mifuncion() {
Mutex cerrojo = new Mutex();
lock(cerrojo);
scope(exit) unlock(cerrojo);
funcionpeligrosa();
}

Con este código estamos indicando que cuando salgamos ("exit") del ámbito ("scope") actual, se ejecutará unlock(). Usando exit garantizamos que el unlock se ejecutará en cualquier caso al salir, sea esta salida de forma normal o por una excepción (sin interrumpir el flujo de esta, por lo que no hace falta relanzarla.) Existen varios especificadores de ámbito para scope, que son:

  • sucess: Se ejecutará cuando se salga con éxito (sin excepción.)
  • failure: Se ejecutará cuando se salga con error (con una excepción.)
  • exit: Se ejecutará cuando se salga del ámbito en cualquier caso (éxito o error.)

Realmente, usando las guardas de ámbito, la instrucción finally nunca es necesaria aunque se sigue manteniendo en el lenguaje. Por comparación, la "antigua" forma de hacer esto sería:

    void mifuncion() {
try {
Mutex cerrojo = new Mutex();
lock(cerrojo);
funcionpeligrosa();
} finally {
unlock(cerrojo);
throw(o);
}

Las guardas de inclusión no sustituyen a la instrucción except; para capturar excepciones y realizar operaciones en la captura debemos seguir usándola.

Para los Pythoneros, la guarda de ámbito de éxito (sucess) sería equivalente a el "else" de las excepciones en Python.

Estructuración del código en módulos y paquetes

En C/C++ la estructuración del código se hace creando "ficheros de cabecera" con extensión .h o .hpp que contienen las declaraciones de los símbolos que cada subparte explorta, y luego esos ficheros de cabecera se importan usando la directiva del preprocesador #include. Esta importación lo único que hace es incluir el código del .h importado allí donde se encuentra la directiva import. Esto lleva a problemas cuando varios ficheros de cabecera definen un mismo símbolo, con soluciones poco elegantes.

C++ añade además el concepto de namespaces que permite agrupar los símbolos de distintos ficheros en un único espacio de nombres lógico.

En D, como en casi todos los lenguajes modernos, la estructuración del código y las bibliotecas se hace usando módulos y paquetes. Un módulo no es más que un fichero fuente de código D (generalmente con extensión .d).

Importar símbolos de un módulo con import

Para que un módulo pueda acceder a los símbolos de otro módulo se usa la sentencia import:

---- inicio modulo1.d ----
module modulo1;

int simbolomodulo1 = 10;
---- fin modulo1.d ----

---- inicio modulo2.d ----
module modulo2;

import modulo1;

writefln( simbolomodulo1 ); // correcto
---- fin modulo2.d ----

Como puede verse, al contrario de lo que sucede en otros lenguajes, cuando importamos los símbolos de un módulo no hace falta anteponer el nombre del módulo con un punto antes de llamar a un símbolo; en modulo2 podemos ver que hemos usado simbolomodulo1 en lugar de modulo1.simbolomodulo1 como se haría en otros lenguajes. A pesar de ello la notación de modulo.simbolo se permite también en D, y es necesaria útil cuando se importan dos módulos que definen símbolos homónimos para indicar el módulo del cual queremos usar el símbolo; no hacerlo así en ese caso es un error de compilación.

Además, también se puede ver que al principio de cada modulo se usa la palabra clave module seguida del nombre del módulo. Esta sentencia es opcional y para módulos no pertenecientes a paquetes no suele ser necesario pues por defecto se toma como nombre del módulo el nombre del fichero sin la extensión. Si existe, debe ser la primera sentencia (comentarios aparte) del fichero fuente.

static import

Si quieremos que los símbolos importados sean accedidos siempre mediante la notación modulo.símbolo usaremos un static import:

---- inicio modulo2.d ---
static import modulo1;

// writefln( simbolomodulo1 ); // error
writefln( modulo1.simbolomodulo1 ); // correcto

import public y private

Las palabras clave public y private antepuestas a un import significan:

public
Los símbolos que se incluyen a través del import serán a su vez exportados a los módulos que importen el módulo donde está el import.
private
Los símbolos que se incluyen a través del import no estarán accesibles en los módulos que importen a su vez el módulo donde está el import. Este es el comportamiento por defecto por lo que no es necesario escribir esta palabra clave.

Ejemplo:

---- inicio modulo2.d ----
import modulo1; // private import por defecto

int simbolomodulo2 = 20;

writefln( simbolomodulo1 );
---- fin modulo2.d ----

---- inicio modulo3 ----
import modulo2;

writefln( simbolomodulo2 ); // correcto, simbolomodulo2 esta definido en modulo 2
writefln( simbolomodulo1 ); // error, simbolomodulo1 está definido en módulo1,
// y como modulo3 no importa a modulo1 no puede
//acceder al módulo1 aunque modulo2 lo importe
//y modulo3 importe modulo2

Para que no diera error:

---- inicio modulo2.d ----
public import modulo1; // ahora exportamos todos los símbolos de modulo1
// como si fueran de modulo2

int simbolomodulo2 = 20;

---- fin modulo2.d ----

---- inicio modulo3 ----
import modulo2;

// correcto, simbolomodulo2 esta definido en modulo 2:
writefln( simbolomodulo2 );
// correcto porque modulo1 esta importado públicamente en modulo2:
writefln( simbolomodulo1 );

Renombrado de módulos

Los módulos pueden renombrarse cuando se importan usando el signo igual con el nuevo nombre a la izquierda y el nombre del módulo renombrado a la derecha:

import corto = mipaquete.directorio.nombreDeModuloMuyLargo

import selectivo de símbolos

En lugar de importar todos los símbolos de un módulo, existe la posiblidad de importar sólo algunos símbolos específicos. Esto es útil cuando sólo vamos a usar uno o dos símbolos de un módulo concreto. La sintáxis para ello es poner el signo dos puntos con el nombre del módulo a la izquierda y la lista de símbolos que se importan del mismo a la derecha, separados por comas.

// Sólo importamos writef y writefln de std.stdio
import std.stdio: writef, writefln

También podemos importar símbolos selectivamente al tiempo que los renombramos:

// Sólo importamos writef y writefln renombrando el último a escribelinea
import std.stdio: writef, escribelinea = writefln

Acceso a símbolos ocultados por símbolos locales

En ocasiones queremos poder acceder a un símbolo del propio módulo en el que está el código que lo usa, pero este puede haberse visto ocultado por un símbolo en el alcance local. En ese caso tan sólo tenemos que usar la notación "punto símbolo" (.símbolo) que seria equivalente a hacer nombredemodulo.símbolo desde otro módulo que importara el actual:

int simbolo;

void funcionconlocal(int simbolo) {
writefln( simbolo ); // escribe el parámetro simbolo
writefln( .simbolo ); // escribe el símbolo del módulo

Paquetes

En ocasiones el código se necesita una estructuración que vaya más allá de los módulos. En ese caso se puede hacer una jerarquía de módulos bajo un concepto mayor llamado "paquete". Para declarar que un módulo está en un paquete debemos incluir la ruta que debe tener el módulo dentro de la jerarquía del paquete, separando los distintos niveles de la misma y el nombre del módulo (al final) mediante puntos. Generalmente la jerarquía de los paquetes suele corresponderse con una organización de directorios y subdirectorios equivalente, aunque esto no es estrictamente necesario.

---- fichero conexiones.d, que está en el directorio mibiblioteca/red
module mibiblioteca.red.conexiones

//etc
---- fichero que está en el directorio mibiblioteca/red/servidores
module mibiblioteca.red.servidores.http

//etc

Donde se buscan los módulos y paquetes

Cuando escribimos "import nombremodulo" el compilador primero buscará "nombremodulo.d" en el directorio actual. De no encontrarlo, si hemos proporcionado uno o más parámetros -Idirectorio al compilador "nombremodulo.d" se buscará en dichos directorios. Finalmente si tampoco se encuentra en ellos se buscará en el -Idirectorio que esté especificado en el fichero dmd.conf.

Cuando se trata de paquetes, si el paquete está organizado por directorios sólo es necesario especificar el nombre raíz donde se encuentra el paquete, no los subdirectorio. Por ejemplo si escribimos import mibib.red.conexion y el raíz del paquete mibib está en /usr/include/dmd/mibib (en el caso de un sistema Unix/Linux/Mac) basta con especificar como parámetro al compilador o en el dmd.conf -I/usr/include/dmd/mibib y el compilador buscará sólo el subdirectorio "red" y dentro del mismo el fichero "conexion.d".

Compatiblidad de llamada con C

Debido a la difusión del lenguaje, la mayoría de las APIs de sistemas operativos y librerías de sistemas están escritas en C u ofrecen una interfaz para el mismo. D puede acceder a bibliotecas de C. Para ello, además de enlazar con el fichero binario de la biblioteca mediante parámetros al compilador, debemos incluir en nuestro código D una declaración de los símbolos y funciones de C a los que queramos acceder. Como los tipos de datos de D suelen tener una correspondencia muy directa con los de C este proceso suele ser bastante sencillo; sin embargo para conversiones de ficheros de cabecera .h más complicados se dispone de la herramienta htod que realiza la conversión de tipos y sintáxis de forma automática, tomando como entrada un fichero .h de C y generando un fichero .d que podemos incluir en nuestros proyectos.

Para las declaraciones de símbolos y funciones de C, debemos poner la instrucción "extern(C):". Tomando un ejemplo de la descripción del htod, si en un fichero de cabecera .h tuviéramos el siguiente código C:

unsigned u;
#define MYINT int
void bar(int x, long y, long long z);

La conversión del mismo en D sería:

extern(C):
uint u;
alias int MYINT;
void bar(int x, int y, long z);

Es importante destacar que dentro de un fichero fuente D no podemos incluir código C (al contrario de lo que sucede en C++.)

En la versión actúal de D no existe la posibilidad de enlazar contra bibliotecas C++.

Delegados, funciones anidadas y funciones literales

En D existe un tipo de dato llamado "delegado" que puede usarse para pasar referencias a un método de una clase como parámetros para otras funciones y métodos. Son, en concepto, similares a los punteros a método de C++ pero con una sintaxis tanto de declaración como de creación y uso mucho más sencilla:

class Foo {
[...]
bool esPar(int N) { return (n % 2 == 0); }
bool esImpar(int N) { return !(n % 2 == 0); }
[...]
}

bool validadora( bool delegate(int) dlg, int valor ) {
return dlg(valor);
}

void main() {
Foo f = new Foo();
bool delegate(int) dlgespar;
bool delegate(int) dlgesimpar;

dlgespar = f.esPar;
dlgesimpar = f.esImpar

bool unoespar = validadora(dlgespar, 1);
bool unoesimpar = validadora(dlgesimpar, 1);

Como puede verse en el ejemplo anterior la declaración de una variable que apunte a un delegado es similar a la de una función pero poniendo "delegate" en lugar del nombre y el nombre de la variable apuntadora al final.

Hablando de funciones y métodos, D permite tener funciones anidadas y funciones anónimas. Las primeras son funciones que están definidas dentro de otra función. Son muy útiles para estructurar nuestro código de una forma más jerárquica.

Las funciones anónimas son funciones (normalmente sencillas) sin nombre que suelen utilizarse como argumento para una función que espera recibir una función como argumento.

Declaración anticipada de funciones innecesaria

En C y C++ para que una función pueda llamar a otra esta debe haber sido declarada con anterioridad a la misma. Para ello puede estar el cuerpo completo de la función, o sólo un "prototipo" que indica los tipos aceptados y el valor devuelto, por ejemplo esto no funcionaría:

void llamaotra(int prueba) {
funcionprimera( prueba ); // error, funcionprimera no está definida
}

void funcionprimera(int param) { return param; }

Por lo que tendríamos que declarar funcionprimera antes de implementarla, o directamente implementar "más arriba" de llamaotra.

El compilador de D es algo más inteligente y hace que esto sea innecesario; cualquier función puede llamar a cualquier otra que esté definida en el programa, independiéntemente de su posición en el código.

Compilación condicional y versionado sin necesidad de un preprocesador

En C++, como en C, se utiliza un preprocesador que analiza el código realizando modificaciones e inclusiones sobre el mismo antes de que se inicie la compilación. El preprocesador de C/C++ es antiguo, y además algunos programadores parecen disfrutar escribiendo código enrevesado usando las instrucciones que el mismo proporciona, lo cual lleva a código bastante ilegible y dificil de seguir y depurar.

D elimina completamente el preprocesador y en lugar de esos añade una serie de directivas para el compilador pero incluidas en el lenguaje que nos permiten darle indicaciones sobre que código compilar según unos parámetros determinables en tiempo de compilación.

Estas directivas son:

module

Con esta intrucción indicamos el uso desde nuestro programa de otros módulos en C (ver más adelante el apartado sobre módulos y paquetes para ver una descripción más completa) y por lo tanto sustituiría al #include de C/C++. Al contrario de lo que sucede con el #include no hay ningún problema en que varios módulos incluyan a un mismo símbolo a través de importaciones, por lo que el chapucero código siguiente, tan necesario en C/C++ para proyectos complicados que involucran muchas inclusiones, es innecesario en D:

#ifndef _TENEMOS_MODULO_

//código del módulo

#define _TENEMOS_MODULO_

#endif
//Nada

version

Otro uso muy común del preprocesador de C/C++ es la supresión o no de determinados bloques de código según parámetros pasados al compilador o incluidos en el entorno del mismo. Por ejemplo en C/C++ cuando queremos que un código se compile en Windows o Linux hacemos:

#ifdef linux
printf("estoy en linux!");

#ifdef windows
printf("estoy en windows!");

En este caso la definición "linux" o "windows" lo proporciona el compilador, pero igualmente se pueden suminitrar al mismo definiciones adicionales usando la linea de comandos del mismo.

En D para hacer esto mismo, pero con más elegancia, se usa la instrucción version:

version(linux){
printf("Estoy en linux!");
}

version(windows){
printf("Estoy en windows!");
}

Podemos definir los símbolos comprobables mediante la intrucción version usando parámetros al compilador (-version=shareware) o podemos generar nuevos símbolos dentro del código, siempre usando bloques evaluables en tiempo de compilación, como el incluido dentro de otro bloque version, usando asignaciones:

version(shareware) {
version = puedeleer;
}

version(comercial) {
version = puedeleer;
version = puedeescribir;
version = puedeimprimir;
}


version(puedeleer) {
//Implementación de la funcionalidad de lectura
}

version(puedeescribir) {
//Impplementación de la funcionalidad de escritura
}

version(puedeimprimir) {
//Implementacion de la funcionalidad de impresión
}

En el ejemplo anterior, al compilar especificaremos como parámetro al compilador "version=shareware" o "version=comercial" según la versión del programa que queramos que se genere.

debug

La instrucción debug es muy similar a version pero en lugar de usarse para especificar características del entorno o funcionalidades se usa para incluir dentro del bloque código de depuración que nos ayude a detectar y corregir errores en versiones en desarrollo. Sustituye a otro uso tradicional del preprocesador en C/C++:

#define DEBUG

//codigo normal

#ifdef DEBUG
//codigo de depuracion
#endif

En D el código equivalente sería:

debug = si

//código normal
debug(si) {
//código de depuración
}

La intrucción debug además de aceptar como parámetro un posible identificador, acepta valores enteros. En ese caso se compilarán todos los bloques debug cuyo parámetro sea igual o menor al valor entero. Por ejemplo:

debug = 2

// código normal

debug(1) {
// código de depuración de nivel 1
}

debug(2) {
// código de depuración de nivel 2
}

debug(3) {
// código de depuración de nivel 3
}

En el ejemplo anterior, como debug vale 2, se ejecutarán los dos primeros bloques de depuración (los que tienen como parámetro 1 y 2), pero no se ejecutará el tercero. Esto es útil para incrementar escalar la salida de diagnóstico de nuestro programa en varios niveles, siendo las mayores las que más salida mostrarán.

static if

El static if se usa para comprobar el valor de símbolos evaluables en tiempo de compilación, como constantes o alias. Es similar a version, pero permite una mayor flexibilidad al poder usarse en instanciaciones de templates.

const int bitsint = 16;

static if (bitsint == 16)
alias short INT;

else static if (bitsint == 32)
alias int INT;

static assert

El static assert funciona igual que el assert pero comprobado sólo valores evaluables en tiempo de compilación. En caso de que la condición evalue a un valor falso, el programa no se compilará.

Arrays mucho más manejables y foreach

Los arrays (o matrices, o vectores, como se quieran traducir) en C++ son exactamente los mismos que en C, es decir, un poco de azúcar sintáctico para un puntero a una memoria asignada. El azúcar sintáctico sin embargo no es mucho, y para realizar algunas operaciones básicas (como añadir o quitar elementos a un array o cambiar su tamaño) debemos hacer engorrosas llamadas para asignar e inicializar nueva memoria. La cosa empeora si pensamos que además las "cadenas" en C están implementadas como un array de caracteres sin ninguna propiedad especial.

C++ soluciona parte de estas limitaciones a través de su librería estándar de plantillas (STL) que proporciona plantillas de clase genéricas para diversas estructuras de datos con múltiples operaciones, además varias clases avanzadas para cadenas.

En D, además de existir una biblioteca de plantillas de clase avanzadas (la DTL, actualmente en desarrollo) los arrays cuentan con una serie de características que los hacen mucho más prácticos y manejables que los de C/C++:

  • Cuentan con comprobación de límites.
  • Pueden redimensionarse dinámicamente cambiando su propiedad "length" o añadiendo al final con el operador "~=".
  • Pueden concatenarse fácilmente dos o más arrays en uno sólo con el operador "~".
  • Permiten la especificación de subrangos dentro de un array. Por ejemplo si quisiéramos los elementos 3, 4 y 5 de un array en un subarray podríamos hacer: subarray = miarray[3..5]. Esto además permite copiar de forma sencilla los arrays por valor con la siguiente notación: int array2[] = array1[]; Los rangos también permiten asignaciones automáticas a varios elementos de un array simultaneamente: array[2..6] = 0;
  • Además de la propiedad length, ya comentada, cuentan con otras propiedades útiles (como size para el tamaño, dup para crear un duplicado, reverse para invertir el orden de los elementos y sort para ordenarlos.)
  • Los arrays estáticos (en los que en la declaración indicamos el tamaño de cada dimensión como en int[3][4] matriz; se implementan como las matrices de fortran en lugar de como punteros a punteros; esto hace que sean mucho más eficientes a la hora de calcularlos. Los arrays dinámicos (int[][] matriz;) sin embargo sí que se implementan como en C, como punteros a punteros. Por lo tanto si necesitamos hacer cálculos de matrices extremadamente rápidos es aconsejable usar los arrays estáticos.

En general todas estas características hacen que los arrays en D se manejen de la forma más intuitiva y no haya que estar recordando constantemente que su implementación es poco más que un puntero con longitud (que lo es.)

Cadenas

Una consecuencia directa de estos arrays mejorados es que las cadenas nativas, a pesar de seguir siendo arrays de caracteres, son ahora infinítamente más manejables. Lo único que tenemos que tener en consideración es que los literales de cadena (caracteres entre comillas) son arrays inmutables, esto es, que no pueden ser modificadas y como D no deja asignar entre objetos mutables e inmutables sin hacer conversiones, no podemos asignar los literales de cadena a arrays de char si no los declaramos mutables. Esto que suena tan lioso lo vamos a ver enseguida con un par de ejemplos:

    char[] cadenaMutable; // Cadena es un array de caracteres mutable;
char[] otraCadenaMutable;
cadenaMutable = "hoygan!"; // ERROR: "hoygan!" como literal de cadena es inmutable y no podemos asignarlo a un mutable
otraCadenaMutable = "hoygan!".dup // Correcto, dup crea una copia mutable del literal

Para asignar literales de cadena sin llamar a .dup podríamos declarar a los arrays de caracteres como invariantes:

    invariant(char)[] cadenaInmutable = "hoygan!"; // Correcto, asignamos un inmutable a otro
invariant(char)[] otraCadenaInmutable = cadenaMutable; // ERROR, asignando mutable a inmutable
invariant(char)[] ultimaCadenaInmutable = cadenaMutable.idup; // Correcto, idup crea una copia inmutable

Como esta síntaxis es un poco fea para manejar cadenas y D es un lenguaje orientado principalmente a la practicidad, se ha creado un alias a invariant(char)[] llamado string, de modo que el ejemplo anterior quedaría como:

    string cadenaInmutable = "hoygan!"; // Correcto, asignamos un inmutable a otro
string otraCadenaInmutable = cadenaMutable; // ERROR, asignando mutable a inmutable
string ultimaCadenaInmutable = cadenaMutable.idup; // Correcto, idup crea una copia inmutable

Como he comentado al principio de este punto, las cadenas al no ser más que arrays de char en D pueden tienen las mismas operaciones que éstos (y el mismo rendimiento), por lo tanto podemos olvidarnos del infierno de las cadenas en C:

    // Asignación   
cadena1 = cadena2;

// Copia
string cadena1 = cadena2.dup; //


// Copia de subcadenas
string cadena1 = "Hoygan amijos!";
string cadena2 = cadena1[0..5]; // cadena2 = "Hoygan"

// Concatenación
string cadena3 = cadena1 ~ cadena2;


// Añadir al final
string cadena4 = "Hoygan! ";
cadena4 ~= "amijos!";

// Comparación
if (cadena1 > cadena2) ...

Casi casi igual que en el infierno de punteros y news de C o la verborrea infame de Java o las STL ¿verdad?

Arrays asociativos

D también cuenta con un tipo de dato derivado del array llamado array asociativo que consiste en una estructura de datos (nativa) que asocia una clave con un valor, de forma que después conociendo la clave podamos recuperar el valor. Tanto la clave como el valor pueden ser de cualquier tipo y la forma general de declarar un array asociativo es muy sencilla:

tipoValor[tipoClave];

Por ejemplo para declarar un array asociativo en el que las claves fueran enteros y los valores objetos de tipo "MiClase":

MiClave[int];

O uno con claves de tipo entero y valores de tipo cadena:

int[ string ];

Quien tenga algo de experiencia programando seguramente reconocerá la utilidad de este tipo de estructuras de datos, probablemente una de las más utilizadas en programación (con permiso de las listas/vectores), por ello casi todos los lenguajes de programación ofrecen al programador la posibilidad de usar este tipo. Java, C# y C++ no son una excepción, pero a diferencia de D en estos lenguajes el tipo de array asociativo (y sus variantes) están implementados como clases en la biblioteca estándar por lo que por un lado el rendimiento nunca va a ser el mismo que el de un tipo nativo (optimizable hasta la muerte por el compilador, que sabe manejarlo mejor que una clase) ni por otro lado será igual de cómodo, aunque esto sea menos importante (en el caso de estos lenguajes tendremos que declarar un objeto, construirlo con new y después utilizar métodos para acceder a sus elementos. C++ usando sobrecarga de operadores permitirá usar las instancias con notación de array, pero la declaración y construcción deberán seguir realizándose como objetos de clase; objetos de clase de plantilla, para ser exactos.)

Los arrays asociativos de D además de algunas de las propiedades de los arrays normales, cuentan con algunas adicionales:

  • Podemos llamar a su miembro remove(clave) para eliminar una clave del array.
  • Podemos usar el operador in para comprobar si una clave está en un array (if("polompos" in miArray)...)
  • La propiedad .keys devolverá un array normal que contendrá todas las claves.
  • La propiedad .values devolverá un array normal que contendrá todos los valores.

Aparte de estos tipos nativos (arrays y arrays asociativos) D también contará con una librería de estructuras de datos genéricas que proporcionará estructuras de datos más avanzadas llamada la "D template library" en intenso desarrollo en el momento de escribir este artículo.

foreach y foreach_reverse

Una de las operaciones más comunes con arrays es recorrerlos mediante un bucle for como en el siguiente ejemplo:

for (int i = 0;i < 10; ++i) {
printf("Array[%d]: %d", i, miarray[i]);
}

D añade una palabra clave (heredada de otros lenguajes) que nos permite recorrer los arrays u otros contenedores obteniendo en cada iteración el siguiente elemento en lugar de un índice que luego tendríamos que usar para direccionar cada elemento dentro del bucle. Esto elimina, en la mayor parte de los casos, la necesidad de tener que usar incómodos iteradores, compárese por ejemplo:

TipoContenedor::iterator sl;
for( sl = instanciaCont->begin(); sl != instanciaCont->end(); ++sl)
{
(*sl)->imprimirValor();
}

o...

TipoContenedorIterator it(*instaciaCont);
TipoDato *p;
while( (p = it.current()) != 0)
{
++it;
p->imprimirValor();
}

Con la construcción equivalente en D usando foreach:

foreach(TipoDato t; instanciaCont)
{
t.imprimirValor();
}

En D, por supuesto, también pueden usarse iteradores para formas menos comunes de recorrer los bucles permitiendo toda la potencia de los iteradores "tradicionales" pero rara vez son necesarios porque habitualmente las propias implementaciones de los contenedores permiten recorrerlos de distintas formas:

// Recorrer invertido:
foreach(TipoDato t; instanciaCont.reverse())
{
t.imprimirValor();
}

// Recorrer invertido los valores dobles y pares:
foreach(TipoDato t; instanciacont.filter(pares).transform(doblar).reverse())
{
t.imprimirValor();
}

En el último ejemplo hemos utilizado dos métodos de las instancias de el tipo de dato que estábamos utilizando que aceptan un argumento función o delegado (método) para realizar, en el primer caso una selección de los elementos pares únicamente y el segundo dobla los elementos que queden después de la primera transformación. Como puede verse esto no hace nada que no pueda hacerse con iteradores, pero la sintaxis es mucho más clara y concisa (de nuevo, si realmente necesitamos de iteradores también dispondremos de ellos en D.)

Los métodos mostrados en los últimos ejemplos están implementados en las estructuras de datos de la librería DTL que probablemente formará parte en el futuro, como librería de estructuras de datos, de la librería estándar de D.

También contamos con foreach_reverse que recorre un contenedor del final al inicio por lo que la llamada a reverse() del ejemplo anterior no sería realmente necesaria si usáramemos foreach_reverse en lugar de foreach.

RTTI (identificación de tipos en tiempo de ejecución)

De cuenta con un sencillo mecanismo de RTTI; los objetos disponen de una propiedad "classinfo" la cual contiene algunas propiedades de la clase que se pueden ver completamente en el código del fichero object.d (en general es aconsejable ver ese fichero fuente para ver las capacidades por defecto del objeto raíz de D.) Estas propiedades son:

  • init: Inicializador estático de clase.
  • name: Nombre de la clase
  • vtbl: Array de punteros a las funciones virtuales
  • base: Objeto classinfo de la clase padre
  • destructor: Puntero al destructor
  • deallocator: Punto al desasignador de memoria.
  • defaultContructor: Puntero al constructor por defecto
  • find (método): ¿Para buscar en la jerarquía de la clase?

La versión actual de D no soporta reflexión dentro del lenguaje, aunque algunas librerías de terceros lo están implementando fuera del mismo, por ejemplo Flectioned.

Posibilidad de ejecución de código como si fuera un lenguaje interpretado

Los lenguajes interpretados cuentan con la ventaja de que es muy fácil escribir unas líneas de código en un editor y ejecutar el intérprete sobre el mismo, o inclúir en los sistemas Unix/Linux/Mac como primeras líneas el nombre del intérprete para permitir su ejecución como si de un ejecutable se tratase.

Esta segunda posibilidad está incorporada en D mediante el parámetro al compilador "-run" que compila, enlaza y ejecuta el código que se le pase, sin generar ningún ejecutable en el disco. Ello nos permite crear scripts rápidos que se ejecuten como si de un lenguaje interpretado se tratase simplemente teniendo como primeras líneas:

#!/usr/bin/dmd -run
[Resto de código D]

El compilador de D de Digital Mars es además sorprendentemente rápido, por lo que incluso ejecutándose de esta forma el resultado en ocasiones puede ser más rápido que usando un lenguaje de script real (como Python o Perl.)

Propiedades

Con este tema voy a expandirme un poco más de que sería necesario para explicar las propiedades en D, en mi intento de intentar educar a algunos programadores recalcitrantes.

Muchos programadores inteligentes estarán hasta las narices de leer y escribir "getters" y "setters", que son miembros de clases cuya utilidad es "obtener" (get) o "establecer" (set) el valor de un miembro de una clase. De esto se abusa terriblemente, sobre todo programadores provenientes de los mundos Java y C++, dándose en la realidad casos absurdos como:

    // C++, para no mancillar el D con este ejemplo:
// Persona.h:

class Persona {
public:
void Persona(string nombre, string apellidos);

string getNombre();
void setNombre(string nombre);

string getApellidos();
void setApellido(string apellidos);
private:
string m_nombre;
string m_apellidos;
}

// Persona.cpp:

void Persona::Persona(string nombre, string apellidos) {
m_nombre = nombre;
m_apellidos = apellidos;
}

string getNombre() {
return m_nombre;
}

void setNombre(string nombre) {
m_nombre = nombre;
}

string getApellidos() {
return m_apellidos;
}

string setApellidos(string apellidos) {
m_apellidos = apellidos;
}

// main:
Persona* juan = new Persona("Juan", "Alvarez Martinez");
juan->setName("Juanjo");
juan->setApellidos("Alvarez Martinez");
cout << "Nombre de la persona: " <<>getName();
cout << "Apellidos: " <<>getApellidos();

En realidad, si sabemos que nunca vamos a realizar operaciones sobre el nombre o los apellidos antes de asignarlos y, sobre todo, antes de devolverlos ¿porque escribimos un par de métodos para usar esos miembros? Si lo pensamos bien, en la mayoría de las clases que hemos escrito o visto de esta forma, el 95% de los getters/setters que en origen tienen esta forma jamás harán ninguna operación aparte de asignar el valor o devolverlo directamente. Es decir, esta clase podría haberse escrito directamente así:

    // En D, al ser un ejemplo más digno, además así no hay que escribir dos ficheros:

class Persona {
public:
this(string nombrearg, string apellidosarg) {
nombre = nombrearg;
apellidos = apellidosarg;
}

string nombre;
string apellidos;
}

// Usaremos esto como:
Persona juan = new Persona("Juan", "Alvarez Martinez");
juan.nombre = "Juanjo";
juan.apellidos = "Alvarez Martinez Torres Jimenez";
writefln("Nombre: ", juan.nombre);
writefln("Apellidos: ", juan.apellidos);

Imagino que muchos programadores de Java y C++ estarán tras ver este ejemplo respirando en una bolsa de plástico, sin embargo, aparte de que estas construcciones son muy frecuentes en lenguajes más modernos como Python, si lo pensamos detenidamente no tiene ningún sentido escribir funciones alrededor de variables cuyo valor nunca se va a modificar dentro de dichas funciones. Ninguno en absoluto. Se que alguno estará arañándose la cara y gritando mientras mira al cielo "¡La encapsulación, la encapsulación!" como si la encapsulación fuera el Dios de una religión que hay que seguir por una cuestión de fe, pero es que resulta que no estamos rompiendo ninguna encapsulación porque este (adorado por algunos) término no significa más que encapsular un miembro de la clase y, asignar directamente un valor y devolverlo sin hacerle nada no encapsula nada en absoluto. Y hace que el código sea más lento, por cierto.

Hay sin embargo un argumento bastante bueno castigar al usuario de nuestra clase haciéndole escribir getters y setters infinitos: aunque en la versión actual de la clase no realizemos ninguna operación al obtener o devolver el valor miembro, es posible que en el futuro sí queramos hacerlo. Por ejemplo podemos querer asegurarnos de que la primera letra del nombre y los apellidos sea mayúscula, o que al devolver el nombre añadamos el "Don" o "Doña". En realidad aunque muchos se agarren a este argumento la inmensa mayoría de getters y setters que empiezan vacíos pasa el resto de sus días vacíos. Pero ante la duda, incluso programadores más racionales y menos dogmáticos, usan los getters del demonio para evitar el riesgo de romper la interfaz de la clase en el futuro.

Propiedades al rescate. Las propiedades en D y otros lenguajes son unas construcciones que nos permiten ofrecer al usuario de la clase una interfaz sencilla (juan.nombre = "Juanjo"; writefln(juan.nombre);) pero al mismo tiempo nos permiten tener la posibilidad de realizar operaciones además de la asignación o devolución del valor, si así lo estimamos oportuno. Y lo mejor de todo es que nos permiten no escribir función alguna, usando un miembro público, mientras no se realicen esas operaciones adicionales, y todo ello sin romper nunca la interfaz de nuestra clase. Si repasamos el ejemplo anterior, podríamos pensar que si en la versión 2.0 de Persona queremos que al asignarse Persona.nombre se ponga siempre la primera letra en mayúscula no podemos hacerlo sin meter los gesetters del infierno. Pero usando propiedades sí podemos:

    // En D, al ser un ejemplo más digno, además así no hay que escribir dos ficheros:
import std.string;

class Persona {
public:
this(string nombrearg, string apellidosarg) {
nombre = nombrearg;
apellidos = apellidosarg;
}

string nombre() { return m_nombre; } // "Getter"
void nombre(string nombrearg) {
m_nombre = capitalize(nombrearg);
}

private:

string m_nombre;
string m_apellidos;
}

// Y el usuario lo sigue usando, EXACTAMENTE IGUAL, pero
// nótese como pone la primera letra mayúscula

Persona juan = new Persona("Juan", "Alvarez Martinez");
juan.nombre = "juanjo";
juan.apellidos = "Alvarez Martinez Torres Jimenez";
writefln("Nombre: ", juan.nombre); // Imprime "Juanjo", no "juanjo"
writefln("Apellidos: ", juan.apellidos);

Sinceramente, el que no vea la belleza y elegancia de esto y siga prefiriendo los gesetters al estilo tradicional, mejor que se dedique a una profesión donde no haya que evolucionar tanto como en la programación.

--> Leer más...

..:: Acerca de este blog ::..

Este blog fue diseñado por el Lic. Felipe Aguilera - Lic. en Análisis de Sistemas. Cualquier duda o consulta comunicarse con: msfa001@gmail.com