
Stratego: Un lenguaje de programación para el manejo de programas
Traducido por Abdiel Isaí Izquierdo Osorio
Los lenguajes de programación tienen un doble papel en la construcción de software. El lenguaje es tanto nuestra base (la esencia de la cual hacemos software), como nuestra herramienta (la que usamos para elaborar software). La transformación de programas (PT, por sus siglas en inglés) tiene que ver con el análisis, el manejo y la generación de software. Por lo tanto, existe una estrecha relación entre la transformación de programas y los lenguajes de programación, hasta el punto en el cual los campos de la PT han producido muchos lenguajes cuya área específica es el manejo de programas. En este artículo, les mostraré algunos aspectos interesantes de uno de esos lenguajes: Stratego.
Introducción
En muchas industrias, el objetivo es entregar un producto satisfactorio dentro del presupuesto y a tiempo. Por alguna razón esto es raro en la industria del software. La teoría de que el software está frecuentemente retrasado, caro, lleno de errores y desbordado no es ciertamente nueva. El pionero Edsger Dijkstra llamó a este fenómeno la crisis del software en su conferencia del Premio Turing de ACM 1972. En esencia, se refiere a la dificultad de escribir correcta, comprensible y comprobablemente programas de computadora. La dificultad proviene de la complejidad, expectativas y el cambio [1]. Esta crisis crónica ha sido inspiración tanto directa como indirecta de muchas brillantes ideas en los pasados 30 años, tal vez las especialmente relacionadas con los lenguajes de programación.
Dado el lugar central de los lenguajes de programación como base y herramienta, es razonable gastar parte de nuestros recursos colectivos en busca de mejoras en la manera en que diseñamos, implementamos y aplicamos nuestros lenguajes. Este fundamento es simple: mejorando nuestros lenguajes, mejoramos a los que probablemente son los dos más importantes pilares técnicos de la construcción de software.
Comparándonos con las otras industrias, tenemos algo de camino por recorrer. La elaboración de software aún no es una disciplina genuina de ingeniería. No podemos actuar de acuerdo a un conjunto establecido de normas y esperar obtener un resultado previsto. La construcción en otras disciplinas es una actividad sistemática y regular, mientras que la construcción de software no lo es en absoluto.
Las razones para esta situación son muchas y complejas. Dado nuestro contexto --lenguajes de programación-- me concentraré en cómo abordar el cambio y la complejidad en el software usando técnicas y herramientas del campo de la transformación de programas. Les iniciaré en el lenguaje cuya área específica es la transformación de programas, Stratego, y daré una percepción del por qué la transformación de los lenguajes actuales es suficientemente complicada para justificar su propio lenguaje. El viaje termina con una corta discusión del estado de la investigación en el mejoramiento de las representaciones que usamos para el software, y en cómo este trabajo puede mejorar considerablemente las capacidades de los sistemas de transformación de programas.
Preámbulo
La ingeniería de software parece no tener metodologías adecuadas, bien fundadas y establecidas para la construcción de software. El resultado es que los proyectos aun fallan en todas las etapas de desarrollo. Una falla puede manifestarse en cualquier fase del desarrollo: análisis de los requerimientos, especificaciones o diseño del software. Las fallas con frecuencia ocurren también debido a problemas en otros aspectos del desarrollo, como una mala comunicación entre el cliente y el diseñador. Para cada aspecto y fase del desarrollo, existen muchas técnicas alternativas y prácticas muy buenas. Estas técnicas y prácticas buenas son frecuentemente controladas por metodologías coherentes; enfoques disciplinados para llegar a la correcta implementación para un problema dado. Es común para las metodologías que sean hechas realidad encima de los lenguajes, los cuales proporcionan cierto grado de soporte de "nivel básico" para los fines promovidos por la metodología. Como veremos, el grado de acoplamiento entre el lenguaje y la metodología varía mucho entre diversas metodologías.
Metodologías
Las metodologías de desarrollo de software incluyen el Modelo Cascada, la Programación Ágil, la Programación Extrema y el Proceso Racional Unificado. El propósito de una metodología de desarrollo es principalmente proveer normas de conducta que son aplicables a través de las herramientas, lenguajes y equipos. En cierto sentido, restringen la diversidad que permitimos cuando expresamos nuestro software, con el objetivo de aumentar los resultados coherentes y disciplinados en el software el cual es más robusto, fácil de mantener y más correcto.
Aunque algunas metodologías vienen con herramientas del cliente que ayudan en el proceso, no son requisitos estrictos cuando se aplica la metodología. De la misma manera, algunas de estas metodologías promueven características específicas del lenguaje para alcanzar sus cualidades, como el encapsulamiento. Esto no significa, sin embargo, que las metodologías no puedan ser usadas con lenguajes que carecen de tales características.
Lenguajes
Otro enfoque para resolver la crisis del software es empezar concentrándonos en lenguajes con los que expresamos nuestro software, saturándolos con buenas cualidades, mientras que dejamos fuera las malas particularidades. Este enfoque ha dado pie a muchas filosofías para la construcción de lenguajes de computadora. Los paradigmas orientados a objetos, orientados a los aspectos, de procedimientos, funcionales, basados en normas o incluso orientados a los lenguajes [3] son descritos tanto en la investigación como en la literatura técnica, y son enseñados en las universidades. Estos paradigmas están todos concentrados en cómo los lenguajes de programación deben ser construidos para maximizar su mantenimiento, extensibilidad, seguridad y otros aspectos del proceso de desarrollo. El reto para el diseñador del lenguaje es encontrar un balance entre las elementales y bien fundadas, disciplina y flexibilidad.
Conseguir este balance correcto en el primer intento es difícil, por consiguiente la evolución de los lenguajes es frecuente. Muy a menudo, esta evolución ocurre a través de la polinización cruzada entre paradigmas: vemos que lenguajes populares pueden acabar con nuevas (viejas) características como tipos nulos comunes, clases internas, lenguajes delegados e incrustados. Estamos siendo testigos de una especie de características progresivas. Guy L. Steele enfatizó la necesidad de lenguajes que evolucionen elegantemente en su disertación Desarrollando un Lenguaje en la OOPSLA'98 (Conferencia ACM de Programación Orientada a Objetos, Sistemas, Lenguajes y Aplicaciones 1998), sin embargo la comprensión de que un lenguaje puede no ser capaz de unirlos todos no es un estudio nuevo. En 1969 y 1971, los simposios fueron sostenidos en temas de lenguajes extensibles ([4], [5]), los cuales nos dan un indicio de que este asunto ha estado con nosotros casi desde los inicios de los lenguajes de programación estructurada.
Enfoques Combinados
Algunos enfoques para resolver la crisis del software entrelazan íntimamente la metodología con el lenguaje. El Método Eiffel, basado en torno al lenguaje de programación orientado a objetos Eiffel, es un ejemplo de este tipo de enfoques. Central en este enfoque es el soporte de contratos. Un contrato en este contexto es una descripción formal del comportamiento de un método, y de datos que no varían en una clase, especificados por el programador. La semejanza más detallada y precisa a un contrato es, que la mayor parte del comportamiento del programa puede ser analizado y probado por adelantado. Tales contratos pueden incluso servir como una base para las optimizaciones y las pruebas (de unidades).
Un tema común: La transformación de programas
Algo común a todos los enfoques en la sección anterior, es que necesitan manejar el problemático trío de la crisis del software: complejidad, cambio y expectativas. Las diversas metodologías atacan estas facetas con técnicas diferentes. La Programación Ágil y la Extrema abarcan el cambio promocionando el uso del refactoring (discutido más abajo) y las pruebas por segmentos; en un intento de minimizar el costo por cambiar el código fuente actual. El Proceso Unificado aboga por el uso de herramientas modeladoras, las cuales generarán el código fuente (semi) automáticamente. Esto se basa en la suposición de que cambiar el modelo es menos propenso a los errores y más rápido que hacer los cambios equivalentes en el nivel del código fuente. Íntimamente unidas a cada enfoque, encontrarás partes esenciales de la transformación de programas.
¿Pero qué es la transformación de programas? Una definición usada con frecuencia es que la transformación de programas es el desarrollo sistemático de programas eficientes de especificaciones de alto nivel manteniendo la importancia del manejo de programas [6]. Esta definición puede ser demasiado limitada para captar la mayor parte de las actividades a las que la transformación de programas es aplicada. Por ejemplo, en su más estricto sentido, no aplicaría al refactoring, o incluso a las herramientas de análisis de código fuente que producen reportes de defectos potenciales. Una definición más precisa y global (tal vez bajo un nuevo nombre, como manejo de programas) debe incluir disposiciones acerca de las actividades fundamentales de la transformación de programas: análisis, manipulación y generación de software.
Análisis
Cada vez que usas un compilador, estás contando con un análisis automático de tu código, tanto sintáctico como semántico. En todos excepto el más trivial de los lenguajes, el análisis semántico es necesario para dar sentido a lo que un programa dado significa. Toma el siguiente fragmento de código de C++ como un ejemplo:
f() {
int a(b);
}
No es posible indicar lo que esta expresión significa en puramente una base sintáctica: esto es, dado solo el fragmento de código como el de arriba, no es posible saber lo que la declaración int a(b); significa, ya que no se sabe nada de b. Si b es un valor (o una variable), simplemente nos indica que el entero a debe ser inicializada con el valor de b. Sin embargo, si b es un tipo, la declaración es una función de declaración y a sería considerada una función que toma un argumento de tipo b que regresa un int. El hecho de que muchas interpretaciones del mismo fragmento de código son posibles, significa que el código es ambiguo. Las herramientas y técnicas para la transformación de programas que cubriré después están bien adaptadas para encargarse de las ambigüedades tanto sintáctica (análisis sintáctico de las sentencias) como semánticamente.
En un mismo sentido, el análisis sintáctico y semántico "extendido" es a menudo calificado de análisis estático. Las herramientas de análisis estático buscan errores comunes de programación y código escrito en un mal estilo. Como tales, pueden ser usados para soportar o incluso imponer aspectos de las metodologías dadas. Por ejemplo, pueden ser usadas para buscar la existencia de patrones mal diseñados (también llamados "anti"-patrones), métodos que son demasiado largos, pares de clases que parecen muy estrechamente acopladas, clases sin documentar, o código que viola el subsistema de encapsulamiento, etcétera. A veces, el análisis de un mal estilo de cifrado es calificado como detección de código incorrecto.
Herramientas bien conocidas para esto incluyen el venerable lint de C/C++, FindBugs [7] y PMD [8]. En el grupo Office en Microsoft, Robert F. Crew construyó un analizador basado en el prólogo de árboles de sintaxis abstracta (los árboles de sintaxis abstracta son discutidos después), el cual ha sido usado para detectar código incorrecto en los productos de Office [9]. El análisis estático también ha hecho su camino dentro de muchos compiladores modernos. Los compiladores MipsPro de C/C++, el compilador Eclipse para Java y Jikes todos proporcionan advertencias de estilo de código además de los reportes de errores normales.
La comprobación del código fuente es otro ejemplo de dónde las técnicas de la transformación de programas son útiles. Muchos ambientes de desarrollo integrado (IDEs, por sus siglas en inglés) proporcionan funcionalidad para la búsqueda estructural en un proyecto. Métodos, campos, clases y paquetes pueden ser buscados en base a donde fueron declarados y referenciados. Otra vez, la base para esta funcionalidad es el análisis del código fuente.
Manipulación
Tal vez la utilización más popular del manejo y transformación de programas es el refactoring que es el cambio de la estructura interna de un programa para incrementar su facilidad de comprensión y su mantenimiento sin cambiar el comportamiento visible del programa. La mayoría de los IDE's tienen alguna noción de refactoring estos días. El requerimiento de que el refactoring preserve el desarrollo, necesita la presencia de información semántica precisa acerca del código. El cambio es normalmente hecho en una representación interna del código fuente, el árbol de sintaxis abstracta, el cual ha sido obtenido a través del análisis sintáctico de las sentencias y el análisis semántico. Los cambios en el AST (árbol de sintaxis abstracta, siglas en inglés) son automáticamente reflejados en el texto del código fuente.
Generación
Comparado al análisis y al manejo, la generación de código necesita mucho menos infraestructura, y es por lo tanto una actividad bastante común, para los programadores, participar en ella. La generación de código de unión de especificaciones textuales, como CORBA IDLs al código de C++ en sí, la generación de código de SQL y Java desde proyectos de base de datos o la generación de código de JavaScript por JSPs y Servlets son todos ejemplos muy conocidos.
En la mayoría de los casos, no obstante, el código fuente generado es tratado como puro texto. Esto siempre deriva en aparentemente errores de poca importancia: una '}' olvidada aquí o unas ' "' allá. El problema es que no existe una forma para determinar estáticamente si el código generado es correcto, y reportarlo al programador. ¿O la hay? Por pura coincidencia, empleando técnicas de la transformación de programas -- el uso de transformaciones con sintaxis concreta -- podemos de hecho asegurarnos de que nunca jamás generaremos un programa sintácticamente inválido otra vez.
Tomados juntos, las facilidades para el análisis, manejo y generación hacen sistemas de transformación de programas bien equipados para manejar la complejidad y el cambio. Sería sensacionalista argumentar que ellos solucionan completamente estos aspectos; la transformación de programas no es la bala de plata. Pero si trabajar en los lenguajes de computadora usando la transformación de programas es muy útil, ¿por qué no está más extendido? La historia es muy conocida. En primer lugar, existe poca conciencia sobre las técnicas y su utilidad, en parte debido a la educación, y en parte debido a la falta de libros del tema accesibles. En segundo lugar, la disponibilidad de herramientas sólidas ha sido mínima. Haré mi parte para rectificar esto haciéndote consciente de un sistema para la transformación de programas: Stratego/XT.
Un lenguaje de programación para el manejo de programas
El entorno Stratego/XT es una caja de herramientas que está compuesta de un lenguaje para el manejo de software: Stratego, y una colección de herramientas de infraestructura para trabajar con objetos de software, XT. Este entorno es usado para construir sistemas de transformación de programas. Tanto el lenguaje como su caja de herramientas acompañante tienen algunas propiedades poco comunes las cuales los hacen ideales para hacer frente a los problemas del análisis, manejo y generación de software. Permítanos colocarlo en el tema considerando el diagrama de un típico sistema de transformación de programas creado con Stratego/XT.
(Solo daré una vista de pájaro de Stratego/XT, para proporcionar una intuición de sus conceptos fundamentales. Los detalles pueden ser encontrados en el tutorial completo [2].)
![]() |
| Figura 1: Anatomía de un típico Sistema de Trasformación de Programas |
Para las personas familiarizadas con los compiladores, este enfoque conducido debería venir como una sorpresa. Una archivo fuente de entrada es convertido en un árbol de sintaxis (concreta) (CST, siglas en inglés). Este es reducido hasta llegar a ser un árbol de sintaxis abstracta (AST) en el paso de abstracción, y es entonces anotado con información de tipo y enlaces contextuales por el paso de análisis semántico. En este punto, el AST tiene toda la información incrustada en él necesaria para las transformaciones arbitrarias. El paso de transformación (el cual puede en realidad ser múltiples pasos en secuencia, o incluso un ciclo de pasos los cuales finalmente terminan) produce un AST final, el cual será publicado en serie en un archivo fuente textual.
El análisis sintáctico del código es una parte crucial de cualquier sistema de transformación de programas, por eso dedicaremos una porción de tiempo a explicarlo. La definición de sintaxis (gramática) es usada para obtener un número de importantes objetos. En primer lugar, es usado por el analizador sintáctico para analizar los archivos fuente textuales. La salida de esta fase es un árbol de sintaxis concreta (CST): una representación de árbol del código fuente, carente de espacios. En segundo lugar, la definición de sintaxis es usada para obtener automáticamente un AST de un CST. La representación del AST despoja toda la sintaxis superflua y solo contiene la esencia del código. En tercer lugar, usamos la definición de sintaxis para obtener un analizador "inverso", o pretty-printer, que convierte un AST en un archivo fuente. El sistema de gramática usado en Stratego/XT es denominado Definición de Sintaxis Formalista (SDF, siglas en ingles). Nos permite construir completamente gramáticas modulares, donde el nivel de modularidad es altamente escalable. Como un ejemplo, el SDF ha sido usado para construir las gramáticas completas de Java 1.4 y 1.5, y también una gramática para AspectJ. El último es una extensión a la gramática de Java, el cual permite separar el mantenimiento y evolución de los dos.
Definiendo la Sintaxis
No voy a proporcionar ninguna gramática realista en este artículo, pero considere la siguiente definición de expresiones aritméticas con variables. La definición está dividida en dos archivos, Lexicals.sdf y Expressions.sdf. Ellos son tratados como módulos separados por las herramientas de SDF, y podemos unirlos usando la directiva imports.
module Lexicals
exports
sorts Nat Id Real
lexical syntax
[0-9]+ "." [0-9]+ -> Real
[0-9]+ -> Nat
[a-z][a-z0-9]* -> Id
|
| Figura 2: Lexicals.sdf |
El archivo en la Figura 2 define tres léxicos (o átomos, si lo prefieres), denominados Real, Nat y Id. Sus sintaxis permitidas son dadas como expresiones regulares.
module Expressions
imports Lexicals
exports
context-free start-symbols Exp
sorts Exp
context-free syntax
Real -> Exp {cons("Real")}
Nat -> Exp {cons("Nat")}
Id -> Exp {cons("Id")}
Exp "+" Exp -> Exp {left, cons("Plus")}
Exp "-" Exp -> Exp {left, cons("Minus")}
Exp "*" Exp -> Exp {left, cons("Times")}
context-free priorities
Exp "*" Exp -> Exp > {left: Exp "+" Exp -> Exp
Exp "-" Exp -> Exp}
|
| Figura 3: Expressions.sdf |
El archivo en la figura 3 define la gramática para las operaciones aritméticas +, - y *. La línea
Real -> Exp {cons("Real")}
dice que Real es un bloque de construcción básico que construye una expresión Exp. También le dice al paso de abstracción en la Figura 1 como construir un AST: haciendo un nodo Real.
Estamos también autorizados para especificar la asociatividad de nuestros operadores. Por ejemplo, la etiqueta left en la línea
Exp "+" Exp -> Exp {left, cons("Plus")}
nos dice que el operador + un asociativo izquierdo. Incluso estamos autorizados declarar la precedencia entre operadores:
context-free priorities
Exp "*" Exp -> Exp > {left: Exp "+" Exp -> Exp
Exp "-" Exp -> Exp}
Analizando expresiones
Armados con la definición de sintaxis de arriba, podemos ahora analizar sintácticamente la siguiente expresión:
0+0.1*a
El árbol de sintaxis abstracta construido para esto está dado en la figura 4.
![]() |
| Figure 4: Árbol de sintaxis abstracta para 0+0.1*a |
En lo siguiente, representaremos tales ASTs textualmente. El AST de la figura 4 puede ser escrito en una notación calificada como Aterms:
Plus(Nat("0"),Times(Real("0.1"),Id("c")))
Manejando ASTs con Stratego
Para llevarse bien con el auténtico manejo de programas, necesitamos empezar modificando nuestros ASTs. He argumentado vagamente que las técnicas de transformación de programas son muy buenas para trabajar en el código fuente de lenguajes de programación normales, pero no he dicho nada acerca de cómo los lenguajes para la transformación de programas deben lucir. De la misma forma en que casi cualquier lenguaje se puede usar para modificar documentos XML, algunos lenguajes, como XSLT, están más adaptados para la tarea que otros. Stratego es un lenguaje adaptado para el manejo de ASTs.
En la jerga de Stratego, los ASTs son llamados términos. Esto tiene sus razones históricas: Stratego está basado en la teoría de sistemas de reescritura y toma prestado mucho de su terminología de este campo. Usamos signaturas para definir nuestros términos (ASTs). Piense en las signaturas como declaraciones de tipo. Una signatura para nuestra expresión aritmética está dada a continuación:
module Arithmetic
signature
constructors
Plus : Exp * Exp -> Exp
Minus : Exp * Exp -> Exp
Times : Exp * Exp -> Exp
Nat : String -> Exp
Id : String -> Exp
Real : String -> Exp
Esta signatura es derivada automáticamente de la definición de sintaxis que escribimos arriba. Esto le dice a Stratego cómo podemos construir términos legales (ASTs): un Plus es un término (nodo) que toma dos subtérminos, ambos de tipo Exp. La notación Aterm de arriba es exactamente la representación que Stratego usa para sus términos, por ejemplo
Plus(Nat("0"),Times(Real("0.1"),Id("c")))
is a valid term according to the Stratego signature we have defined.
¿Entonces cómo modificamos este término? Con funciones llamadas reglas y estrategias. Lo mismo que los programas de Java son construidos a partir de métodos, campos, clases y paquetes, los programas de Stratego son construidos a partir de signaturas, reglas, estrategias y módulos. Una regla es una minitransformación, una reescritura, de un término (a left-hand side) a otro (a right-hand side). Por ejemplo, Nat("0") -> Nat("1") es una regla que reescribe el número natural cero al número natural uno, en nuestra notación. Podemos darle a la regla un nombre, por ejemplo Inc:
Inc:
Nat("0") -> Nat("1")
Observando nuestro término de arriba, vemos que nuestra regla puede ser aplicable, si solo pudiéramos aplicarla en el lugar correcto. ¿Cómo hacemos eso? Es aquí donde las estrategias entran. Una estrategia es una función que controla la aplicación de las reglas. En nuestro entusiasmo por el cambio, podemos ser tentados a aplicar nuestras reglas por todo el lugar, esperando que quedarán bien en alguna parte. La siguiente estrategia hace exactamente eso.
topdown(try(Inc))
La estrategia de arriba recorre el término (piense en al árbol de la figura 4) en un orden de arriba abajo (preorden) tratando de aplicar nuestra Inc a todos los nodos. Cuando Inc tiene éxito, mantendrá el resultado. Cuando Inc falla (porque no estamos en un término Nat(0)), la estrategia se omitirá. Después que hayamos aplicado la estrategia a nuestro término, obtenemos el no tan sorprendente resultado:
Plus(Nat("1"),Times(Real("0.1"),Id("c")))
La estrategia topdown es llamada una travesía, porque atraviesa un término. Hay otras travesías en la biblioteca de Stratego, como bottomup, innermost y outermost. Son todas utilizadas para seleccionar el orden en que un subtérmino es visitado.
La forma estándar de hacer travesías de árbol en Java (o C#, y muchos otros lenguajes orientado a objetos ‘OO’), es usar visitantes. Cuando una travesía de árbol llega a ser una operación frecuente y básica en tus programas, la notación OO para los visitantes resulta en una incontrolable explosión de código.
La figura 5 muestra un programa completo de Stratego incorporando todo el código de arriba. Después de la compilación con el compilador de Stratego, resultará en un solo programa que acepta un ATerm como uno de los de arriba en stdin y escribirá un ATerm transformado a stdout.
module Example
imports lib
signature
constructors
Plus : Exp * Exp -> Exp
Minus : Exp * Exp -> Exp
Times : Exp * Exp -> Exp
Nat : String -> Exp
Id : String -> Exp
Real : String -> Exp
rules
Inc:
Nat("0") -> Nat("1")
strategies
main = io-wrap(topdown(try(Inc)))
|
| Figura 5: Un programa completo de Stratego |
Uniendo muchas de estas transformaciones en una serie, a través de los canales (Unix), obtenemos el conducto de la figura 1. El entorno Stratego/XT viene con una totalidad componentes enteros de infraestructura que resume estos detalles, llamados XTC. Usando XTC, cada cuadro en la figura 1 llega a ser un componente XT, y el lenguaje de unión de componentes XT puede ser usado para crear flexibles y avanzados conductos de transformación.
Transformaciones Genéricas
Los ejemplos que mostramos anteriormente son todos conceptualmente muy simples. Es importante darse cuenta de que Stratego es útil también para el trabajo rudo, como el análisis semántico de los programas de Java y C++, o incluso su optimización y compilación. Mucho trabajo ha sido invertido en estas áreas. Afortunadamente, resulta ser que partes esenciales de este trabajo rudo son descomponibles e incluso generalizables.
Los frutos de esta labor pueden ser encontrados en la biblioteca de Stratego. Las transformaciones genéricas (piense en ellas semejantes a muy pequeños estructuras de OO) son ahora disponibles para su reutilización. Conectando su gramática del lenguaje y el código de unión en estas transformaciones, puedes obtener un nombre de ámbito apropiado así como el análisis y control del flujo de datos muy fácilmente.
Sintaxis Concreta
Afirmé que los errores de sintaxis en los generadores de código eran cosa del pasado. Una técnica poderosa para tratar con esto en Stratego/XT es el uso de la sintaxis concreta [10] dentro de los programas de Stratego. Permítanos considerar otra regla, la DouglasOperation, la cual debe ser aplicada a la expresión 6*7. Esto por supuesto resultará en 42. En la notación Aterm, tal regla se verá:
DouglasOperation:
Times(Nat("6"), Nat("7")) -> Nat("42")
Si bien esta notación es muy precisa, es una forma inusual de escribir aritmética, y no se escala bien a expresiones grandes. La alternativa ofrecida por la sintaxis concreta es escribir la regla usando el lenguaje para el cual ya tenemos una gramática:
DouglasOperation: |[6*7]| -> |[42]|
Encerrada entre corchetes especiales |[]| está la sintaxis concreta para los términos que queremos. Con la directiva adecuada, el compilador de Stratego sabrá qué gramática usar para analizar el contenido de tales corchetes, y automáticamente expandirá |[6*7]| a Times(Nat("6"),Nat("7")) secretamente. Más importante, el lado derecho también será expandido a un término válido. Si un término válido no puede ser obtenido, por ejemplo la expresión dentro de los corchetes especiales es inválida; obtendremos una advertencia de tiempo de compilación. De esta forma, sabemos antes del despliegue que nuestra generación de código está por lo menos sintácticamente correcta.
Entorno
El entorno de Stratego/XT es similar a muchos otros entornos de lenguaje, por lo tanto no entraré en detalles. El entorno proporciona un editor interactivo (Spoofax), un compilador, un intérprete, así como herramientas de documentación de código fuente (xDoc) al estilo de Javadoc.
Estructuras de datos para el manejo de programas
Como dije antes, la transformación de programas no es una bala de plata, y viene con algunos problemas propios. Quizá es más importante el hecho de que no hay una estructura de datos perfecta para la representación y el manejo de software. Los programas son estructuras extremadamente ricas y entrelazadas. Una razón para esto es la presencia de cantidades considerables de complejidad accidental en los diseños de lenguajes de computadora, por lo menos cuando es visto desde nuestro punto de vista transformacional. Mencioné el problema de ambigüedad anteriormente; otro es la falta de ortogonalidad. Por ejemplo, en muchos lenguajes derivados de Algol, como C, C++, Java y C#, no existe diferencia entre los ciclos for y while. Puedes con poco esfuerzo reescribir uno por el otro. (foreach should no debe ser considerado equivalente a for/while: no podemos reescribir un while arbitrario en un foreach sin recurrir al ingenio de codificar y a la generación de código adicional). La irritación surge de tener siempre que tratar con ambas formas cuando escribimos transformaciones.
La falta de ortogonalidad también exacerba el asunto de efectos laterales no locales. Si su transformación recibe un término (AST) anotado con información semántica, puedes rápidamente invalidar toda esta información haciendo un cambio de poca importancia. Parte de esto es esencial y deseable: el ámbito del nombre es considerado una propiedad deseable. Si renombras una variable, debes renombrar también todos sus usos en los ámbitos inferiores. Esto es simple dentro métodos y clases, pero renombrar un campo o método en una clase requiere un poco más de manipulación.
La no ortogonalidad y la ambigüedad son ejemplos fáciles de comprender que son visibles en la punta del iceberg de síntomas. El hecho es que pocos lenguajes contemporáneos fueron diseñados para ser fácilmente transformables, y esto a veces viene a hacernos mella. Encontrar una forma bien fundada para la abstracción sobre la complejidad accidental, y mantener una consistencia no local es aún un problema de investigación.
![]() |
| Figura 6: Árboles de sintaxis abstracta de alto nivel |
Una manera de encargarse del tema de la abstracción - reubicando el for y while, considerándolos a ambos como ciclos infinitos (potenciales) -- es indicada en la figura 6. Después del análisis semántico, un AST de alto nivel es producido, donde la complejidad accidental ha sido reducida. En este nivel, tanto el for y while están ahora generalizados en un término UnboundedLoop adherido a la siguiente signatura, la cual es esencialmente un while:
UnboundedLoop: Exp * CodeBlock
En el caso de foreach y muchos ciclos for podemos sacar límites superiores e inferiores, así como una variable de estimulación. Cuando esto es posible, podemos poner más estructura en nuestro ciclo, y convertirlo en un BoundedLoop en vez de:
BoundedLoop: Lower * Upper * Step * CodeBlock
Esta variante es como el estilo de Pascal del ciclo for y se abre para muchas transformaciones de ciclo que son tanto muy difíciles como evidentemente imposible en un UnboundedLoop.
Transformaciones más abstractas (y simplificadas) son expresadas en este AST de alto nivel, preferentemente usando transformaciones genéricas donde es posible. Existen casos en donde tratar con la complejidad accidental es necesario, por ejemplo cuando hacemos refactoring. Cuando movemos el código, necesitamos mantener los fors como fors, pero al mismo tiempo llevar a cabo nuestro análisis de visibilidad tan simple como sea posible. En este caso la conmutación entre el nivel alto y el nivel bajo en representaciones AST es útil, y poder localizar el origen de un UnboundedLoop es importante. Todos estos son temas aun bajo investigación.
Conclusión
En este punto, espero que tenga una apreciación de qué es la transformación de programas, por qué está íntimamente unida a los lenguajes de programación, y cómo cabe en el cuadro de la ingeniería de software tratando dos de las tres causas de la crisis del software, denominadas complejidad y cambio.
Además, la transformación de programas trae consigo lenguajes de programación propios. Si bien los diminutos ejemplos de Stratego son más bien estériles, muestran cómo la notación de dominio específico de Stratego nos permite expresar concisamente operaciones en términos. Ellos ilustran que Stratego es un lenguaje preciso para el manejo de software.
La reescritura de términos, el fundamento matemático en el que está basado Stratego (el cual, por educación, he mantenido fuera de este artículo), data de hace décadas, y está estrechamente relacionada a la teoría detrás de la programación funcional. Ambas teorías proporcionan un fondo de conocimiento estable en el cual construir lenguajes de transformación de programas, herramientas y técnicas.
La frontera imperfecta del campo de la transformación de programas está delineada por un frente de investigación. Muchos de los temas en este frente están compartidos entre otras disciplinas, como cómo organizar y modularizar programas para la transformación de programas. Detallé un tema que es específico al campo, a saber la búsqueda de una representación óptima de programas.
Como con cualquier paradigma, comprar al mayoreo la transformación de programas no es apropiado probablemente. De lo que deberías ser consciente, sin embargo, es que el campo es un tesoro escondido de técnicas para el análisis, generación y manejo de software. Esto es visto en los muchos ejemplos donde las técnicas de la transformación de programas han sido exitosamente transplantadas en otras partes de la ingeniería de software.
El entorno de Stratego/XT libremente disponible en www.stratego-language.org, y funciona en la mayoría de las plataformas.
Reconocimientos
Deseo agradecer a Eelco Visser por sus comentarios perspicaces
sobre este artículo, y a Andrew David, el diligente editor
de ACM quien me ayudo a ordenar este manuscrito para su
publicación.
Referencias
- 1
- Entrada de Wikipedia. The software crisis
- 2
- M. Bravenboer, K.T. Kalleberg, E. Visser. The Stratego/XT Tutorial
- 3
- Language-Oriented Programming se refiere al uso dominante de lenguajes de dominio específico para resolver bien definidas y complicadas tareas, al estilo de Unix' "little languages"".
- 4
- Carlos Christensen and Christopher J. Shaw, editors, Proceedings of the Extensible Languages Symposium, Boston, Massachusetts, May 13, 1969 [SIGPLAN Notices 4 no. 8 (August 1969)]
- 5
- Stephen A. Schuman, Proceedings of the International Symposium on Extensible Languages, Grenoble, France, September 6-8, 1971 [SIGPLAN Notices 6 no. 12 (December 1971)].
- 6
- Paraphrased from Partsch, H. and Steinbrüggen, R. 1983. Program Transformation Systems. ACM Comput. Surv. 15, 3 (Sep. 1983), 199-236. DOI= http://doi.acm.org/10.1145/356914.356917
- 7
- FindBugs - http://findbugs.sourceforge.net
- 8
- PMD - http://pmd.sourceforge.net
- 9
- R. E Crew. ASTLOG: A language for examining abstract syntax trees. In Proc. of the First Conf. on Domain Specific Languages, pages 229-242, Oct. 1997.
- 10
- E. Visser. Meta-programming with concrete object syntax. In Generative Programming and Component Engineering (GPCE) Conference, LNCS 2487, pages 299--315. Springer, 2002.
Biografía
Karl Trygve Kalleberg es un estudiante de doctorado en la Universidad de Bergen, Noruega, investigando representaciones de programas mejoradas. Pasó su último año académico en Universiteit Utrecht en los Paises Bajos, trabajando en el entorno de Stratego/XT. Es financiado por el Consejo de Investigación de Noruega.
Abdiel Isaí Izquierdo Osorio (abd_izai@yahoo.com.mx) Abdiel Isaí Izquierdo Osorio, es estudiante de Ingeniería en Sistemas, en la Universidad del Valle de Mexico, Campus Villahermosa, Tabasco, México, y miembro del capítulo estudiantil de la misma universidad.


