jueves, 10 de enero de 2013

¿Todos para uno o uno para todo?

Ya expliqué hace poco que PSeInt está compuesto por varios programas separados, la mayoría de ellos independientes. El usuario percibe al conjunto de programas como si fuera uno, con varias partes trabajando en conjunto. wxPSeInt es el programa que se encarga de ser la interfaz con el usuario, y de gestionar la ejecución de los otros de forma más o menos transparente. Veamos ahora porqué esto es así y qué tiene de interesante o no, según mi experiencia con este modelo.

Empezando por el porqué, tengo que decir que es más bien una cuestión histórica. Primero que nada, lo que quería escribir cuando empecé con PSeInt era solo un intérprete (el módulo pseint), el editor de texto surgió inmediatamente como agregado necesario para poder mostrar las bondades del intérprete, pero los demás módulos no estaban previstos. Desde ese punto de vista, tendría solo dos módulos, un editor de texto y un intérprete de consola. La comunicación sería mínima: el editor guardaría el pseudocódigo en algún temporal y lanzaría el intérprete diciéndole por argumento en la linea de comandos donde estaba ese temporal. Si había errores, el camino de vuelta también sería mediante un archivo temporal. Esto sonaba razonable, y este era más o menos el modelo que seguían la mayoría de los IDEs en lenguajes reales, así que jamás se me cruzó por la cabeza plantear otra alternativa. Mucho tiempo después se fueron agregando algunos módulos nuevos que tampoco requerían de mayor comunicación, como el visualizador (no editor) de diagramas de flujo, o el que exporta a código C++. Por eso seguí con el mismo esquema. Tal vez la primer duda respecto a si este era el camino a seguir vino cuando quise integrar la ejecución paso a paso, y también más tarde para el análisis sintáctico en tiempo real.

Para la ejecución paso a paso necesitaba una comunicación más fluida, ya que el usuario interactúa con la interfaz del editor, y no directamente con el intérprete. Había pensado originalmente que el editor y el intérprete compartirían algún bloque de memoria, donde el intérprete guardaría su estado para que el editor lo consulte. Pero el uso de memoria compartida resultó no ser del todo práctico para esto, había muchas cuestiones de implementación que no había considerado. Para ese punto, era mucho trabajo unir editor e intérprete en un solo programa, así que busqué otra alternativa y terminé experimentando con comunicaciones tcp/ip locales.

Para el análisis de sintaxis en tiempo real, me hubiese gustado tener el intérprete bien diseñado, de forma de hacer con sus principales funciones una biblioteca que pueda utilizar también el editor. Sin embargo, el código del intérprete era un desastre y esto no era posible ni seguro, así que seguí con la filosofía de mantener las partes separadas, pero tuve que buscar una nueva forma de comunicación. Utilizar archivos temporales para lanzar el intérprete y pasarle el algoritmo para que lo analice no era buena idea en este caso, porque esto es algo que iba a tener que hacerse muchísimas veces y rápido. Lanzar el proceso y escribir archivos por cada mínimo cambio del pseudocódigo no es buena idea. Por eso opté por utilizar tuberías entre procesos (pipes). Esto significa que un proceso controla la entrada y salida estándar del otro (siendo este otro un programa de consola). Entonces, el intérprete tiene un modo especial en el que toma el pseudocódigo desde el "teclado", muestra los errores en la "pantalla", y luego vuelve a su estado inicial. La interfáz lanza una vez este proceso y utiliza siempre la misma tubería, de forma que no hay archivos intermediarios, y no hay procesos lanzándose a cada rato, sino que todo va y viene por la memoria simplemente. Resulta rápido, y sólo tuve que implementar un mecanismo de señalización muy muy elemental en las entradas y salidas para que los procesos se entiendan.

Para la edición del diagrama de flujos, seguí un enfoque híbrido entre archivos y conexiones tcp/ip. Los archivos son para llevar y traer el pseudocódigo entre el editor de texto y el de diagrama, cosa que no ocurre con tanta frecuencia, sino solo cuando se abre o cierra el segundo editor. La conexión tcp/ip es solo para "notificar" eventos, como por ejemplo cuando el editor de texto le pide al de diagrama que pase al frente, o cuando el de diagrama le avisa al de texto que ya actualizó el archivo de intercambio y quiere que lo mande a ejecutar.

Finalmente, el caso de la nueva terminal para que la ejecución refleje rápidamente los cambios del algoritmo, es el caso que queda por resolver. La forma fácil de hacerlo reutilizando lo existente involucra archivos para llevar el código del editor al intérprete, y agrega solo una comunicación tcp/ip para notificar cosas de forma similar al caso del editor de diagramas de flujo. Pero si quiero que los cambios se reflejen rápidamente, estoy en un caso similar al de la verificación de sintaxis en tiempo real, donde usar archivos y relanzar el proceso no es buena idea. Por eso no es "inmediato", sino que se toma unos segundos más que la verificación de sintaxis, para evitar "fatigar" al disco con tantas idas y venidas, y a la cpu relazando constantemente el mismo proceso.


Intentando extraer alguna conclusión, puedo decir que lo malo de tener todo separado es que hay que trabajar para sincronizar todos los procesos, combinar varios mecanismos de comunicación, implementar pequeños protocolos de notificación y señalización, y que alguien (wxPSeInt) tiene que controlar todo esto y decidir cuando se lanza cada proceso. Todo esto además conlleva cierta sobrecarga que puede ralentizar el proceso. Sin embargo, ese trabajo extra se compensa con las ventajas. Poder desarrollar y probar los módulos por separados algo muy deseable. Esto permite actualizar o directamente reemplazar módulos individuales casi sin cambiar los demás. Podrían por ejemplo, utilizar el intérprete en conjunto con cualquier otro editor de texto (de hecho ya reimplementé tres veces el editor, con builder, con gtk y con wx,  sin cambios en el intérprete).

Y lo mejor de todo es que acota de alguna forma el impacto de las metidas de pata. Si un módulo revienta por algún error, mientras no sea el que controla todo, los demás pueden seguir su tarea sin perder los datos. Si todo estuviera junto, un segfault por un error en la lógica de dibujo del diagrama, por ejemplo, forzaría el cierre de todo el sistema PSeInt, provocando la perdida de todos los algoritmos abiertos en el momento. Estando separado, solo se pierden los cambios de ese algoritmo, y solo los realizados en el editor de diagramas. De forma similar, los errores en el parseo o en la interpretación de las instrucciones también quedan acotados. Muchas veces un alumno que está aprendiendo ingresa instrucciones con construcciones inesperadas, que jamás se me abrían ocurrido, y que ponen de manifiesto fallas en el parseo que revientan el intérprete. Nuevamente, en este escenario la separación minimiza el daño.

Todos los mecanismos presentados tienen entonces una (espero que buena) justificación. El más cuestionable es el último, y tendré que repensarlo si quiero seguir experimentando en la dirección que planteaba Bret Victor.

No hay comentarios:

Publicar un comentario