Características del Lenguaje D

Violeta and Hexa build an in-memory database in D, using compile-time reflection to extract field names and types automatically, custom attributes for constraints, and CTFE to parse and validate SQL queries at compile time. They generate projection structures dynamically with string mixing and return lazy ranges for on-demand results.
Chapters
Parece que hoy estamos solas, Hexa, así que podemos decidir de qué hablar. Quizás algo de concurrencia, programación distribuida, goffers. ¿Te hace?
Estamos solas, pero tenemos una responsabilidad. La opción más votada ha hablado: las características del lenguaje D. ¿El lenguaje D?
¿Otra vez?
No empieces como Chip, Violeta. El lenguaje D fue un vídeo genial, pero no hablamos de lo más importante.
Hablamos de su historia, su nombre, curiosidades... ¡Ah, ya recuerdo! Chip cortó antes de tiempo y tú querías hablar de sus características.
Totalmente, así que ponte un café, que este vídeo será intenso. Hace un par de vídeos que no decimos nada de las votaciones, ¿y ya nos abandonáis?
No creo que nos hayan abandonado, Hexa, solo que tendrán cosas más importantes que hacer. De todas formas, todavía tenemos a nuestros seguidores más fieles, haciendo votaciones.
Pues eso deberían de hacer todos. Venga, id a la página de votaciones, echad un vistazo a la opción que más os guste y a votar.
Y se puede emitir un voto por día.
Y ya que estáis ahí, echad un vistazo al curso de Zig, que os gustará.
¿Hemos mezclado votaciones con cursos? ¿No deberíamos decir una sola cosa?
Y tenemos subidos nuevos capítulos en modo podcast en Spotify.
Hexa, tranquila, tómate mejor una tila.
Las características de D. La tila puede esperar. Abre el editor, que vamos a construir una pequeña base de datos.
¿Una base de datos? Pensaba que íbamos a ver las características de D. Precisamente.
Una pequeñita, en memoria. Guardaremos usuarios, pondremos restricciones y haremos consultas. Así cada característica tendrá algo que resolver.
Vale, pero como acabemos implementando Postgres, la que va a necesitar la tila voy a ser yo.
Empezamos con una estructura: identificador, nombre, correo y edad.
Hasta aquí nada nuevo.
Ahora creamos una tabla de usuarios.
¡Espera! ¿Y esa exclamación entre Table y Usuario?
Es la sintaxis de De para pasar argumentos a una plantilla. Nuestra plantilla Table define una tabla genérica. Aquí le pasamos el tipo Usuario para especializarla. También podríamos rodear Usuario de paréntesis después de la exclamación, pero en este caso De permite omitirlos.
¿Y si mañana queremos guardar productos? ¿Le pasamos otro tipo?
Totalmente. Insertamos nuestros 4 usuarios y vamos a mostrar tanto el esquema como las filas.
¡Un momento! ¿De dónde sale ese esquema? Hemos definido los campos en usuario, pero no le hemos dado a la tabla una lista de columnas.
Porque podemos obtenerla del propio tipo. Vamos al motor. Esta función recibe el tipo como argumento de plantilla. Y aquí viene lo interesante. La plantilla FieldNameChupel de la biblioteca estándar nos proporciona con los nombres de los campos de un tipo. Con usuario obtenemos id, nombre y medida.
¿Estamos usando reflexión?
Sí, reflexión en tiempo de compilación. El compilador ya conoce la estructura del tipo y podemos utilizar esa información desde nuestro código.
Entonces, ¿ese bucle static foreach no recorre las columnas cuando ejecutamos el programa?
Ese recorrido se resuelve al compilar. Su cuerpo se expande para cada campo. Las operaciones resultantes, que construyen nuestra lista de textos, sí se ejecutarán cuando llamemos a la función.
¡Ah, vale! El compilador decide qué operaciones habrá, después el programa las ejecuta.
Totalmente. El mecanismo de reflexión del lenguaje, traits, nos permite consultar información que conoce el compilador. Aquí usamos su operación get_member para referirnos a un campo por su nombre. La palabra clave typeof obtiene el tipo de ese campo y la propiedad string_of nos da el tipo como texto.
¿Y ese t.init? ¿Todavía no tenemos ningún usuario ahí?
La propiedad init nos da el valor inicial predeterminado del tipo. Aquí la usamos sobre t, el tipo recibido por la plantilla, para consultar el tipo de cada campo sin necesitar una fila guardada.
O sea, que no estamos adivinando el tipo por el contenido de los datos.
El tipo ya está definido. Para mostrar una fila hacemos algo parecido. Esta vez obtenemos el valor de cada campo del registro que nos han pasado y lo convertimos en texto.
Entonces tampoco escribimos una función distinta para imprimir cada clase de registro.
Y nuestra tarea La tabla, de momento, es sencilla: guarda los registros en un array, permite insertarlos y ofrece una vista de solo lectura.
Bien, ya tenemos dónde guardarlos y cómo inspeccionarlos.
Dejamos también unas pruebas. Aquí comprobamos, entre otras cosas, que la reflexión funciona con un tipo diferente: un producto con su código y su precio. Compilamos, ejecutamos y... ahí están los nombres de las columnas, sus tipos y los 4 usuarios.
Todo a partir de una única definición de los campos. ¡Eso sí me gusta! Si cambia la estructura, no tengo otra lista que acordarme de actualizar.
Y todavía podemos aprovechar más esa información.
Porque ahora mismo podría insertar 2 usuarios con el mismo identificador, ¿verdad?
Sí, eso es lo siguiente que vamos a resolver. Atributos que ponen las reglas. Vamos a marcar el identificador como clave primaria con el atributo PRIMARY KEY y el correo como único con el atributo UNIQUE.
¿Y qué hacen esas anotaciones de ya tiene algo para base de datos?
Estas las definimos nosotros. Son atributos que podemos asociar a una declaración y consultar durante la compilación.
Entonces escribir unique encima de un campo no impide por sí solo que se repita.
No, la anotación expresa nuestra intención. Ahora tenemos que enseñarle al motor qué hacer con ella.
Y para comprobarlo estás intentando insertar otro usuario con mi correo.
Con un identificador distinto además. Así sabremos que lo rechaza por el correo y no por la clave primaria. Aquí definimos primary_key y unique como dos estructuras vacías.
¿Vacías? ¿No necesitan una función que compruebe los duplicados?
Las usamos como marcas, la comprobación pertenece a la tabla. Separar ambas cosas nos permite decidir cómo aplicar esas reglas sin cambiar las anotaciones de los usuarios. Volvemos a recorrer los campos del tipo durante la compilación, pero esta vez preguntamos si cada uno tiene alguno de nuestros atributos.
Eso hace HasUDA, ¿verdad? ¿Qué significa UDA?
Son las siglas en inglés de atributo definido por el usuario. La plantilla HasUDA de la biblioteca estándar comprueba si una declaración tiene el atributo que le indicamos y el condicional de compilación estática if decide si incluimos la comprobación para ese campo.
¿El programa preguntará por la anotación cada vez que insertemos una fila?
No. Esa decisión ya queda resuelta al compilar. Para el identificador y el correo se incluye la comprobación, para el nombre y la edad no.
Pero saber si el correo está repetido sí requiere mirar los datos que tengamos en ese momento.
Como diría Pro, exacto. Este bucle foreach sin static recorre las filas durante la ejecución. Si encuentra el mismo valor, lanza una excepción antes de añadir el registro.
Ahora se ve bien la diferencia. Al compilar decidimos qué campos vigilar. Al insertar comprobamos sus valores.
Totalmente. Y nuestra implementación es sencilla porque solo busca recorriendo las filas. Una base de datos más completa podría utilizar un índice para acelerar esa búsqueda.
¿Y qué diferencia hay aquí entre clave primaria y clave única?
Por ahora ambas marcas exigen que el valor no se repita. No hemos implementado valores nulos ni claves compuestas. Estamos construyendo lo necesario para mostrar cómo funcionan los atributos. Ejecutamos y... el correo duplicado queda rechazado. Seguimos teniendo 4 filas.
Me gusta que la regla se vea justo al campo al que afecta. No tengo que buscarla dentro del motor para saber que el correo debe ser único.
Y el motor aprovecha esa información sin tener una comprobación escrita específicamente para usuario.
Bien, ya podemos guardar datos con algunas garantías. Ahora quiero consultarlos sin tener que recorrer todas las filas.
Vamos a darle una pequeña dosis de SQL. Una consulta que se revisa antes de arrancar. Vamos a pedir el nombre y la edad de los usuarios que tengan 30 años o más.
Esa cadena parece SQL. ¿Ahora tenemos que interpretarla cada vez que consultamos?
Vamos a analizarla durante la compilación. Así podremos comprobar si tiene sentido antes de ejecutar el programa.
Espera, ¿el compilador de D entiende SQL?
¡No! Entiende D, y nosotras vamos a escribir una función en D que analice esta pequeña parte de SQL. Esta estructura que hemos llamado query guarda lo que necesitamos: las columnas solicitadas, la tabla y, si hay filtro, el campo y su valor mínimo. Nuestra función parse_sql separa las palabras y reconoce las partes de la consulta. Solo admitimos esta sintaxis sencilla con un filtro opcional de mayor o igual que un entero.
Estoy viendo bucles, cadenas y una estructura que se va rellenando. Parece una función corriente.
Lo es. Lo interesante viene al utilizarla. Aquí, dentro de nuestra plantilla ChildQuery, usamos la palabra clave enum para declarar una constante cuyo valor procede de llamar a nuestra función. Ese contexto exige obtener el resultado durante la compilación.
¿Enum? Yo esperaba una enumeración de valores.
El lenguaje D también tiene esta forma de declarar una constante calculada al compilar. Para obtenerla, el compilador evalúa nuestra función. A eso se le llama CTFE.
¿CTFE?
Evaluación de funciones en tiempo de compilación. Bueno, en sus siglas en inglés, claro.
O sea, ¿no hemos tenido que escribir una segunda versión del analizador para el compilador?
Totalmente. Esta misma función también podría trabajar durante la ejecución. Aquí la utilizamos en un contexto que exige resolverla antes. Después comprobamos la tabla y las columnas con la instrucción STATIC_ASSERT, que exige que una condición se cumpla durante la compilación. Si pedimos un campo que no existe, la compilación falla con nuestro mensaje.
Entonces una consulta con apodo, que no está en usuario, ni siquiera nos deja construir el programa.
Eso es. Y también comprobamos que el campo del filtro sea un entero.
¿Y los usuarios que cumplen la condición también los encuentra al compilar?
Eso ocurre al ejecutar. Nuestra función query utiliza la consulta ya analizada, pero todavía tiene que recorrer los datos y comparar las edades.
Hemos adelantado el análisis y las comprobaciones que dependen del esquema. Los datos se consultan después.
Y aquí aparecen Violeta y Pro, con sus nombres y edades.
Una cosa, ¿el resultado contiene solo esos 2 campos?
Todavía conserva el usuario completo, pero hemos seleccionado qué mostrar. Ahora vamos a hacer que la propia consulta determine el tipo del resultado. Una consulta que crea su propio tipo. Ahora el resultado tendrá una estructura con los campos que pedimos. La palabra clave alias nos permite darle un nombre al tipo de cada fila, con el nombre fila, valga la redundancia.
¿Y dónde hemos definido esa estructura con nombre y edad?
Nuestra función projection_fields construye un texto con las declaraciones de los campos seleccionados. Aprovecha la palabra clave typeof para conservar sus tipos originales.
Espera, ¿estamos escribiendo código dentro de una cadena?
Sí, pero falta convertir ese texto en parte del programa. Aquí definimos nuestra estructura genérica Projection. Dentro utilizamos la palabra clave mixin para incorporar las declaraciones que genera la función.
¿Incorporar una cadena como código? ¿Eso ocurre al ejecutar?
Durante la compilación. Este mecanismo se llama string mixing. El compilador evalúa la cadena y compila su contenido donde hemos colocado el mixing.
Entonces, ¿la estructura cambia según los campos que pongamos en la consulta?
Totalmente, y los campos conservan sus tipos. Nombre sigue siendo una cadena y edad un entero.
¿Y si el texto generado contiene código incorrecto?
El compilador lo rechaza. Además, nuestra función solo genera declaraciones para los nombres de campos que ya hemos validado. Nuestra función Project copia los valores de cada usuario a esta nueva estructura. Esa copia sí ocurre durante la ejecución.
Es decir, primero construimos el tipo y después lo llenamos con datos.
Aquí vemos el esquema del resultado: nombre y edad. También mostramos las declaraciones que hemos generado para que se vea de dónde salen.
Y el correo ya no está escondido en el resultado. Ese campo ni siquiera existe en su tipo.
Eso lo comprobamos al compilar. Pero aún nos queda otra mejora. Tenemos que evitar preparar todos los resultados cuando solo queremos consumir unos pocos. Resultados bajo demanda. Vamos a contar cuántas filas entregamos al recorrer el resultado.
¿Para comprobar que no las preparamos todas de golpe?
Sí. Nuestra función query ahora devuelve un rango. Obtenemos una secuencia que podemos recorrer. No necesitamos construir antes un array con todos los resultados. Encadenando dos funciones de la biblioteca estándar, filter selecciona las filas que cumplen la condición y map transforma cada una al tipo que generamos.
¡Parecen métodos! ¿Los hemos añadido a nuestra tabla?
No, son funciones externas. D permite escribirlas con un punto, pasando lo que hay a su izquierda como primer argumento. Es la Síntaxis Uniforme de Llamada a Funciones, conocida como UFCS.
¿UFCS? ¿Qué significa?
Uniform Function Call Syntax. En español, síntaxis uniforme de llamada a funciones.
Así podemos leer la consulta como una secuencia de pasos: filtrar y transformar.
Y la transformación es perezosa, es decir, se realiza al acceder a cada resultado.
¿Y la función TAKE limita cuántos consumimos?
Sí, con 2 pedimos, como máximo, 2 resultados. Aquí serán Violeta y Pro. Antes de recorrerlos el contador marca 0, después 2.
Entonces, si pedimos solo uno, se entrega mi fila y la de Pro queda sin solicitar.
Totalmente. Pero este contador mide entregas, no cuántas filas se han examinado. El filtro puede adelantarse buscando coincidencias.
¡Tenemos una consulta que valida sus campos, crea su tipo y entrega los resultados a medida que los pedimos!
Y todo con herramientas del lenguaje y su biblioteca estándar. Vamos a cerrar viendo cómo empezar un proyecto propio.
Conclusiones.
Hemos visto reflexión, atributos propios, funciones evaluadas al compilar, generación de tipos y rangos perezosos. Y cómo encadenar funciones con la anotación de punto.
Vale, quiero probarlo. ¿Qué instalo?
Un compilador como LDC y el gestor de proyectos y dependencias Dub.
¿Y cómo comienzo?
El comando dub init crea el proyecto. Entramos en su carpeta source, escribimos en app.d y usamos dub run para compilar y ejecutar.
¿Y las bibliotecas?
Se añaden con dub add seguido del nombre del paquete. Nuestro ejemplo solo necesita la biblioteca estándar.
¿Para ejecutar el programa terminado también necesito dub?
No, con dub build generamos el ejecutable y después lo lanzamos directamente.
Pues la tila se ha quedado fría.
Totalmente, pero ya tenemos por dónde empezar. Dejaremos en la descripción el enlace al código y notas para poder ejecutarlo.
Bueno, pues es vuestro turno. Como diría Chip, agilizad esos dedos y dejad vuestros comentarios abajo en la caja.
Os ha encantado el lenguaje y ver el código que hemos hecho, ¿verdad?
¿Os ha motivado para crear vuestra propia base de datos?
¿A que vais a utilizar D en vuestro siguiente proyecto?
¡Déjanos tu comentario! Al final ha estado entretenido. Habría esperado hablar y hacer algo con concurrencia, pero la ejecución del código en tiempo de compilación es muy interesante.
¿Ves cómo los capítulos de código son mejores que los de historia?
No sé, Hexa, los chicos le ponen más énfasis a contar historias y la historia tras cada lenguaje, librería, personaje... Pero es verdad que ha sido interesante estar en tu rincón viendo código.
El rincón de Hexa suena bien.
¿Qué tienes en mente?
Continuará.
More from Manuel

Historia de los Lenguajes de Programación: años 1960-1969 #basic #apl #simula #lisp #logo

¿Compilador o intérprete? La historia de una rivalidad que comenzó en los años 40








