viernes, 7 de septiembre de 2012

Próximamente: SubProcesos en PSeInt!

La posibilidad de crear funciones/subprocesos/subrutinas/como-quieran-llamarlos en PSeInt es por lejos la funcionalidad por la que más veces me han preguntado los usuarios. Desde hace años que joden con eso. Y desde hace años que les digo lo mismo: por razones históricas el código base del intérprete está tan mal diseñado que me es imposible agregarlas en ese estado. Pero con el paso del tiempo, muchos cambios internos fueron sucediéndose. Por ejemplo, la clase que administra la memoria virtual del intérprete es completamente nueva, culpa de esto los tipos de datos se gestionan de forma muy diferente a cómo se lo hacía en las primeras versiones, y gracias a esto la evaluación de expresiones a mejorado muchísimo y se ha vuelto también más flexible. Después de muchos intentos, esas partes quedaron listas para un futuro mejor. Seguro que tienen sus errores, pero comparativamente ha sido un gran paso. Por contraparte, el código que analiza la estructura de las instrucciones y que en verdad parsea la sintaxis más allá de las expresiones sigue siendo bastante horrible en su diseño. De a poco voy puliendo la implementación, pero el diseño no ha cambiado sustancialmente. No me gusta el enfoque actual, pero no estoy seguro de que el enfoque teóricamente correcto para un intérprete sea el mejor en este caso. No obstante, repensando el tema de los subprocesos por enésima vez llegué a la conclusión de que la no tan pequeña evolución de la que hablaba ya era sufiente como para intentar implementarlos sin morir en el intento. Puse manos a la obra, y algo salió. En este post quiero presentar con bombos y platillos la tan pedida y flamante funcionalidad, y explicar algunas cuestiones de diseño desde el punto de vista del lenguaje y la interfaz, ya no desde la implementación del intérprete. Hay muchos puntos grises que aún no me terminan de convencer y que posiblemente cambien en el futuro, así que hay que tomar todo como está, en estado experimental, sin terminar, sujeto a cambios, pero igual ya hay disponible en el repositorio git un intérprete capaz de aceptar la definición de funciones/subprocesos por parte del usuario.

Para definir un subproceso, la sintaxis sería más o menos así:
   SubProceso valor_de_retorno = NombreSubProc (arg1, arg2, arg3)
      // ...hacer algo con los argumentos y...

      // ...guardar algo en la variable valor_de_retorno...
   FinSubProceso

Luego, se podría invocar el subproceso como una función más, en cualquier expresión, o utilizar como una instrucción más. Un ejemplo concreto:
   SubProceso R <- Factorial(x)
      R <- 1; i <- x;
      Mientras i>1 Hacer
         R <- R*i;

         i <- i-1;
      FinMientras
   FinSubProceso

   SubProceso x <- LeerDato(cosa)
      Escribir "Ingrese ",cosa,": ";
      Leer x;
   FinSubProceso

   SubProceso ImprimirResultado(x)
      Escribir "El resultado es: ",x
   FinSubProceso

   Proceso UsaFunciones
      dato <- LeerDato("un número entero mayor a 1");
      resultado <- Factorial(dato);
      ImprimirResultado(resultado);
   FinProceso


El ejemplo declara 3 subprocesos. 2 de ellos devuelven cosas y por eso se usan como parte de expresiones, el último no devuelve nada, se lo invoca como a una instrucción más. Para quien conozca el manejo de funciones en otros lenguajes, debo decir que los argumentos se pasan siempre por copia (no hay pasaje por referencia) y que el subproceso retorna un solo valor o ninguno (no puede retornar más de una variable). Estas limitaciones existen, entre otras razones, para no complicar el lenguaje. Por ahora la única sintaxis nueva es la primer línea del subproceso. Tampoco hay variables globales ni #includes. Es la versión más sencilla, pero ya alcanza para introducir un concepto básico de función, para organizar mejor el código, y hasta para resolver algoritmos con recursión. Pero volviendo al tema de pasajes por referenecia vs por copia, un dilema importante es ¿qué hacer con los arreglos? (algo que ni consideré en la implementación actual así que no lo intenten todavía). Si no dejo pasar por referencia, una función nunca podrá modificar un arreglo, ya que para aplicar la modificación en la función principal debería retornarlo y no tiene sentido en el lenguaje actual del pseint asignar una arreglo A a otro B con B<-A, sino que se asignan elemento a elemento. Entonces ahí tengo dudas, ¿hago una excepción en ese caso? ¿agrego una sintaxis alternativa para pasar por referencia? ¿permito asignar arreglos siempre de una (como opción a configurar en el perfil del lenguaje)? Todavía no se.

Lo que más se complica ahora es la interfaz, el editor. El código ahora tiene muchos procesos/subprocesos, entonces surgen problemas. La lista de variables por ejemplo, como estaba no tiene sentido, ya que una variable dentro de una función no es la misma que otra con igual nombre pero en otra función. Una solución posible, y creo que es la que voy a aplicar por el momento, es agrupar la lista de variables por función, en una especie de árbol. Este problema de los ámbitos de las variables se extrapola cuando hablamos del modesto "depurador", la ejecución paso a paso. ¿Qué pasará con la prueba de escritorio? los saltos de ámbito van a confundir y mucho. ¿Y debería agregar un pseudo-backtrace, no? Probablemente las expresiones se evalúen en el ámbito de ejecución actual en cada paso, pero tengo que ver cómo presentar correctamente esa información para que no confunda. Además, ahora habría diferencia entre "step-in" y "step-out" al avanzar un paso. De momento agregaré un checkbox al panel de depuración, creo que sería lo más autoexplicativo para un alumno que nunca usó un depurador real. Otro problema no menor es la integración del editor de diagramas de flujo. No está pensado para editar más de un diagrama a la vez en el mismo algoritmo, así que supongo que al lanzarlo tendrá que preguntar, en caso de que el algoritmo tenga subprocesos, cuál de ellos se quiere editar, y no mostrar ni modificar el resto. Como ven, la implementación del manejo de funciones dentro del núcleo del intérprete no ha sido tan dolorosa como imaginaba, pero la parte que más me confunde ahora es la de la interfaz, ¿cómo hacer que todo esto no agregue complejidad innecesaria para el uso que ya le veníamos dando?

No sé si será lo que imaginaban, pero espero que toooda la gente que escribía preguntando por el uso de subprocesos en PSeInt siga entrando al sitio cada tanto y descubra que su deseo está siendo concedido. Por último, aprovecho para agradecer y pedir disculpas a algunos usuarios (buscando rápido entre mis mails encuentro a Michael Picado Fuentes, Luis Angulo Jiménez y Diego Guerrero Zapata, no se si me olvide de alguien) que me escribieron alguna vez y hace mucho tiempo, antes de que se pongan en marcha todos estos cambios, con la intención de implementar ellos mismos esta funcionalidad. Por mis errores inciales y la carencia absoluta de documentación, no pude ayudarlos demasiado y perdimos una buena oportunidad de trabajar en conjunto. No sirve de mucho ofrecer libremente el código fuente si nadie más puede entenderlo, así que ahora, a medida que cambio y reescribo cosas, intento ir documentando cada vez un poquito más.

Al igual que para el post anterior, abrí un hilo en los foros de PSeInt para que me ayuden a discutir las preguntas que quedaron abiertas, o simplemente opinen lo que les parezca. Como comentario al margen, me llama la atención que por mensajes privados me hayan llegado en estos años tantas solicitudes de modificaciones al lenguaje, y en el foro del post anterior (que es uno de los tres más leídos de la corta historia de cucarachasracing) nadie haya comentado nada todavía. Espero que no se pongan tímidos y participen un poco más. La idea de hacerlo en el foro es justamente que todos puedan leer y comentar todos los puntos de vista, aunque sean anónimos.

13 comentarios:

  1. Disculpá que te jodamos tanto, pero PseInt sería más útil y más funcional. (Alejandro Caro)

    ResponderEliminar
    Respuestas
    1. Lo de "joder" lo puse en el buen sentido. No me molesta que pidan esas cosas, sé que es algo muy útil para muchos y hacía rato que quería implementarlo.

      Eliminar
  2. En la parte de: Entonces ahí tengo dudas, ¿hago una excepción en ese caso? ¿agrego una sintaxis alternativa para pasar por referencia? ¿permito asignar arreglos siempre de una (como opción a configurar en el perfil del lenguaje)? creo que debes agregar una sintaxis alternativa para pasar por referencia.

    ResponderEliminar
  3. Una sugerencia si el subproceso no retorna valores entonces una mejor sintaxis seria no incluir valor de retorno y con el nombre de Accion

    Acción NombreAcción( arg1, arg2, argn )
    ... // ...hacer algo con los argumentos
    FinAcción


    Ahora Si retorna valores nombre SubFuncion

    SubFuncion valor_de_retorno = NombreSubFuncion( arg1, arg2, argn )
    // ...hacer algo con los argumentos y...
    // ...guardar algo en la variable valor_de_retorno...
    FinFuncion

    ResponderEliminar
  4. como se hace un menu con funcion y con vectores eliminar ,busca o mostro

    ResponderEliminar
    Respuestas
    1. http://sourceforge.net/projects/pseint/forums/forum/2368325

      Eliminar
  5. Fabián Flores Vadell27 de octubre de 2012, 12:18

    Hola Pablo. Me llamo Fabián y te aclaro que no soy docente de programación.

    Apoyo la idea ayudar al estudiante a diferenciar entre los conceptos de comando (una acción que produce efectos colaterales) y una consulta (que sólo realiza un cálculo en base a sus argumentos y devuelve un valor sin producir efectos colaterales). Creo que la manera más sencilla de hacerlo es que el pseudo lenguaje ofrezca un soporte sintáctico diferente para sendos conceptos, a partir de palabras claves como "comando" y "consulta" que son palabras con un significado muy claro en comparación con "subproceso", "función" y "procedimiento" (en el sentido que enunció Bertrand Meyer de separación entre comandos y consultas "CQS").

    http://en.wikipedia.org/wiki/Command-query_separation

    Por supuesto, creo que esto debería enseñarse como una pauta y no como una verdad absoluta.

    También me parece que la introducción de soporte para "subprocesos" es un paso fundamental para evitar que los estudiantes al mismo tiempo que aprenden otros conceptos básicos de programación, adquieran malas costumbres en cuanto a la estructuración del programa. La noción de que un programa se divide en partes más o menos independientes, o mejor dicho autónomas, es esencial y me parece fundamental que se aprenda desde el principio.

    Esta característica le permite al docente una oportunidad para introducir, si lo cree conveniente, conceptos fundamentales como por ejemplo: modularización, encapsulamiento/ocultamiento de información, interfaz/signatura/firma de los subprocesos, transparencia referencial (que suena como algo "muy avanzado" y sin embargo es un concepto de lo más simple), cohesión y acoplamiento.

    Saludos.

    ResponderEliminar
  6. Fabián Flores Vadell27 de octubre de 2012, 13:03

    Con respecto al paso de parámetros por referencia, me parece que es cuestión de hacer un buen balance.

    En primer lugar según leí en la documentación y en tus comentarios, psint no tiene variables globales y el pasaje de argumentos es por valor. Esta es una decisión que además de mantener simple el pseudo-lenguaje, tiene implicancias conceptuales importantes que deberían mantenerse: ambas ayudan mucho a evitar la proliferación de cambios de estado "accidentales" en el programa.

    Si no se permite el paso por referencia una función no podría nunca modificar un array, eso es cierto, pero puede ser una alternativa válida que una función que recibe un array como argumento aplique una transformación y devuelva el array resultante. Es decir, se podría enseñar a los estudiantes a crear sus propias funciones map, filter y reduce.

    No es una idea tan alocada, me parece, porque en definitiva incluso en los lenguajes orientados a objetos se utilizan patrones de diseño que presentan la idea de limitar la mutabilidad de las estructuras de datos.

    Los detalles y dificultades que podría presentar una posible implementación se me escapan totalmente, pero conceptualmente no me parece una mala idea, ya que mantiene la idea de evitar efectos secundarios no buscados.

    Por supuesto, yo no tengo la respuesta, sólo espero que tal vez lo que digo te ayude a decidirte por una de las alternativas que manejás.

    Saludos.

    ResponderEliminar
    Respuestas
    1. El problema de que la función devuelva un arreglo es que al tomarlo en la llamada estaríamos asignando un arreglo a otro arreglo (a<-filtra(a)), que es algo que en principio no se puede, ya que les hacemos copiar arreglos elemento a elemento. Pero no está claro si está mal o no permitir eso (en general, no solo para funciones), ya que varios lenguajes lo permiten de una u otra forma. Podría ser algo configurable más adelante.
      Respecto al pasaje por valor/referencia, está incluido, pero con una sintaxis que es opcional para no complicar los ejemplos básicos.

      Eliminar
  7. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  8. //Programa de ejemplo corregido (siempre daba 1):
    SubProceso R <- Factorial(x)
    Definir R, i Como Enteros;
    R <- 1; i <- x;
    Mientras i>1 Hacer
    R <- R*i;
    i <- i-1;
    FinMientras
    FinSubProceso

    SubProceso x <- LeerDato(cosa)
    Definir x Como Entero;
    Escribir "Ingrese ",cosa,": ";
    Leer x;
    FinSubProceso

    SubProceso ImprimirResultado(x)
    Escribir "El resultado es: ",x;
    FinSubProceso

    Proceso UsaFunciones
    Definir dato, resultado Como Entero;
    dato <- LeerDato("un número entero mayor a 1");
    resultado <- Factorial(dato);
    ImprimirResultado(resultado);
    FinProceso

    ResponderEliminar
  9. Gracias por implementarlo. Hoy en día ya funciona muy bien. Continúa mejorandolos

    ResponderEliminar