martes, 31 de mayo de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 3

En este artículo consiste en un pequeño programa que permite comprobar algunas de las cosas que he comentado sobre el Event Dispatch Thread de Java Swing en artículos anteriores.

Haciendo pruebas con el Event Dispatch Thread


El pequeño programa EventDispatchThreadTest (ver código fuente completo aquí) ilustrar fácilmente algunos de los efectos que se producen al bloquear el EDT. Se compone de 2 ventanas (JFrame) llamadas One y Two:
  • En la ventana One hay un botón llamado Long-running task que ejecuta una tarea larga que bloquea el EDT.
  • En la ventana Two se deberían mostrar los efectos de la pulsación de dicho botón.
El botón Long-running task simplemente imprime por consola una línea cada cierto número de iteraciones del bucle. De este modo es una tarea medianamente intensiva en operaciones de entrada/salida y no sólo en consumo de procesador (no conviene hacer pruebas de bloqueo con un simple bucle vacío porque se consumiría el procesador sin facilitar la conmutación entre varios hilos de ejecución).

Al comienzo del oyente del botón se establece el texto del JLabel de la ventana Two a "Left button pressed. Task running...". Sin embargo, jamás se llega a ver ese texto porque hasta que no termina el oyente del botón, no se repinta y al final del evento, se ejecuta el método labelAutoText() para pintar el número de veces que se ha realizado al tarea en ese mismo JLabel.

Además, durante todo ese tiempo, el botón parece pulsado, aunque se haya soltado el botón del ratón y se esté haciendo otras cosas con él.

Otra cosa que se puede comprobar es que el resto de componentes de la GUI no responde mientras la tarea está en ejecución. Por ejemplo, no se puede pulsar el botón Nothing.

Sí se puede mover, maximizar/minimizar y cambiar el tamaño de ambas ventanas. Si se agranda cualquiera de ellas, se verá como el nuevo área queda sin pintar hasta que finaliza la tarea. En este caso, el tamaño, posición y estado (minimizado/maximizado) de las ventanas es controlado por el gestor de ventanas del sistema operativo, pero el repintado no se puede realizar hasta que termine el oyente del botón.

Otra de las cosas que permite comprobar el programa es que mientras la tarea se está ejecutando, no se reciben eventos (y por tanto no se podrán disparar otros oyentes). En especial, si se minimiza y se vuelve a maximizar una de las ventanas mientras se está ejecutando la tarea, los mensajes respectivos se verán todos juntos al final, pero no se recibirán en el momento en el que ocurren.

El programa es muy sencillo y se puede retocar muy fácilmente.

Enlaces


Los enlaces de interés relacionados con esta serie de artículos sobre el EDT están aquí, en el primero de los artículos sobre este tema.


(Actualizado 31/05/2016)

Source code: EventDispatchThreadTest

Código fuente del programa EventDispatchThreadTest

Ver la entrada correspondiente en el siguiente enlace:


Java Swing: Event Dispatch Thread (EDT) - Parte 3

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.util.Date;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class EventDispatchThreadTest {

    private JFrame myOne;
    private JFrame myTwo;
    private JButton myButtonTask;
    private JButton myButtonReset;
    private JButton myButtonNothing;
    private JLabel myLabel;
    private int myClickCount;
    private int myResetCount;

    public EventDispatchThreadTest() {
        super();

        JFrame one= new JFrame("One");
        one.setName( one.getTitle() );
        one.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        one.setSize(300, 400);
        one.setLocation(0, 0);
        JButton buttonTask= new JButton("Long-running task");
        JButton buttonReset= new JButton("Reset click count");
        JPanel onePanel= new JPanel();
        onePanel.setOpaque(true);
        onePanel.setBackground(Color.BLUE);
        onePanel.add(buttonTask);
        onePanel.add(buttonReset);
        one.setContentPane(onePanel);

        WindowListener auxWindowListener= EventDispatchThreadTest.createWindowListener();
        one.addWindowListener(auxWindowListener);

        WindowStateListener auxWindowStateListener= EventDispatchThreadTest.createWindowStateListener();
        one.addWindowStateListener(auxWindowStateListener);

        MouseListener auxMouseListener= EventDispatchThreadTest.createMouseListener();
        onePanel.addMouseListener(auxMouseListener);

        MouseMotionListener auxMouseMotionListener= EventDispatchThreadTest.createMouseMotionListener();
        onePanel.addMouseMotionListener(auxMouseMotionListener);

        JFrame two= new JFrame("Two");
        two.setName( two.getTitle() );
        two.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        two.setSize(300, 400);
        two.setLocation(400, 0);
        JLabel label= new JLabel("Never pressed");
        JButton buttonNothing= new JButton("Nothing");
        JPanel twoPanel= new JPanel();
        twoPanel.setOpaque(true);
        twoPanel.setBackground(Color.BLUE);
        twoPanel.add(label);
        twoPanel.add(buttonNothing);
        two.setContentPane(twoPanel);

        buttonTask.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //This text is never shown.
                EventDispatchThreadTest.this.myLabel.setText("Left button pressed. Task running...");

                for (int i= 0; i < 99999; i++) {
                    String auxString= String.format("Working... %s %s%n", i, new Date(System.currentTimeMillis()));
                    if ((i % 2) == 0) {
                        System.out.print(auxString);
                    }
                }

                EventDispatchThreadTest.this.myClickCount++;
                EventDispatchThreadTest.this.labelAutoText();
                System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
            }
        });

        buttonReset.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                EventDispatchThreadTest.this.myClickCount= 0;
                EventDispatchThreadTest.this.myResetCount++;

                EventDispatchThreadTest.this.labelAutoText();
            }
        });

        this.myOne= one;
        this.myTwo= two;
        this.myButtonTask= buttonTask;
        this.myButtonReset= buttonReset;
        this.myButtonNothing= buttonNothing;
        this.myLabel= label;

        this.myClickCount= 0;
        this.myResetCount= 0;
        this.myOne.setVisible(true);
        this.myTwo.setVisible(true);
    }

    private void labelAutoText() {
        this.myLabel.setText(String.format("Click count: %s. Reset count= %s"
            , this.myClickCount
            , this.myResetCount
        ));
    }

    private static WindowListener createWindowListener() {
        WindowListener auxWindowListener= new WindowListener() {
            @Override
            public void windowOpened(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowClosing(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowClosed(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowIconified(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowDeiconified(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowActivated(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowDeactivated(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxWindowListener;
    }

    private static WindowStateListener createWindowStateListener() {
        WindowStateListener auxWindowStateListener= new WindowStateListener() {
            @Override
            public void windowStateChanged(WindowEvent e) {
                System.out.format("Window[State]Event (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxWindowStateListener;
    }

    private static MouseListener createMouseListener() {
        MouseListener auxMouseListener= new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mousePressed(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxMouseListener;
    }

    private static MouseMotionListener createMouseMotionListener() {
        MouseMotionListener auxMouseMotionListener= new MouseMotionListener() {

            @Override
            public void mouseDragged(MouseEvent e) {
                System.out.format("MouseMotionEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                System.out.format("MouseMotionEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxMouseMotionListener;
    }

    public static void main(String[] args) {
        System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
        //GUI code should be executed in the EDT.
        SwingUtilities.invokeLater(new Runnable()  {
            @Override
            public void run() {
                System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
                new EventDispatchThreadTest();
            }
        });
    }

}

domingo, 29 de mayo de 2016

Recapitulando: año 3

Repasando los artículos del blog escritos a lo largo de este tercer año de existencia, puedes descubrir:


Si has leído alguno, muchas gracias y espero que te haya gustado o que te haya resultado útil.

Y recordar que los comentarios son bienvenidos.

Si quieres ver la recopilación de los años anterior, puedes hacerlo a partir de este enlace:



(Actualizado 29/05/2016)



sábado, 30 de abril de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 2

En este artículo continuo hablando sobre el Event Dispatch Thread de Java Swing del que comencé a hablar en el artículo anterior.

Aunque este artículo pueda dar una impresión muy negativa del uso de Swing, no es mi intención criticar negativamente dicho proyecto ni asustar a quienes estén usándolo, sino tan sólo comentar características que, a mi jucio, deben tenerse muy en cuenta al usarlo para no llevarse sorpresas desagradables y que en la mayoría de la documentación a penas se comentan.

Y en cualquier caso, hay que tener en cuenta que muchas de las reflexiones de este artículos son extrapolables a otros entornos, como por ejemplo, el UI Thread de Android.

Restricción impuesta por el Event Dispatch Thread


Una cuestión clave sobre el funcionamiento de Swing es que todo el código que maneje/acceda a componentes debe ejecutarse dentro el EDT (ver Swing's Threading Policy), lo cual no es ninguna tontería. Los componentes no son Thread safe, es decir, no incluyen código de sincronización para que sean accesibles de forma consistente desde varios hilos.

Eso se debe, fundamentalmente, a razones de eficiencia y complejidad de desarrollo. Por una parte, habría muchísimo overhead (coste extra) computacional provocado por el código de sincronización, que influiría negativamente en la fluidez de la GUI. Y por otra parte, conseguir que la GUI fuese accesible desde varios hilos de forma completamente consistente sería una tarea extremadamente compleja (en este artículo uno de sus creadores reflexiona sobre el tema Multithreaded toolkits: A failed dream?).

Complicaciones


A priori la restricción no parece incómoda, de hecho, en muchas ocasiones los programadores ni nos planteamos el uso de varios hilos. Sin embargo, hay que ser conscientes de que en cuanto se utiliza algún componente para la GUI se usan, de forma implícita, al menos 2 hilos: el principal y el EDT.

En el artículo anterior expliqué lo fácil que es bloquear la GUI realizando alguna tarea costosa (acceso a disco, acceso a red, cálculos muy complejos, etc) dentro del EDT. Para evitar que la GUI se bloquee habrá que realizar las tareas pesadas fuera del EDT, pero sin acceder a los componentes de la GUI desde fuera de él. En otras palabras, gestionar asíncronamente los eventos.

Por ejemplo, al pulsar un botón se podría realizar una consulta sobre una base de datos y rellenar un JTable con los datos obtenidos:
  • El evento de pulsación del botón se recibirá dentro del EDT.
  • El JTable ya podría estar creado y mostrándose vacío.
  • La consulta a la base de datos habrá que hacerla fuera del EDT, desde otro hilo.
  • Para poder rellenar el JTable, habrá que esperar a tener los resultados.
  • Habrá que construir un TableModel con los resultados, ¿dentro o fuera del EDT?
  • Habrá que asignar el nuevo TableModel al JTable, dentro del EDT.

O sea, que habrá que andar "saltando" de un hilo a otro dependiendo de la tarea. ¿Y qué pasa con la GUI mientras se obtienen los datos de la consulta?
  • ¿se bloquea la GUI?
  • ¿se ignora la interacción del usuario (descartando clics de ratón y pulsaciones de teclado)?
  • ¿se muestra algún indicador de que la aplicación está ocupada (como el típico cursor de ratón con un reloj de arena)?
  • ¿se muestra un indicador del progreso de la tarea?
  • ¿la GUI sigue siendo operativa? ¿y si se vuelve a pulsar en el botón de cargar mientras hay una o varias cargas en proceso?

Obviamente, el código resultante ya no es tan sencillo como poner dentro del oyente del botón una tras otra las instrucciones:
Además, implementar alguna de las alternativas para la transición es un problema en sí mismo.

La opción de establecer un modelo nuevo completo no siempre es viable, ya que se pueden tener instalados oyentes interesados en saber cuando se añaden o quitan elementos. Estos oyentes deben ser notificados dentro el EDT. ¿Y si añadir o quitar elementos es una tarea pesada que deba realizarse fuera del EDT? ¿Y si hay sucesos que ocurren fuera del EDT que afecten a la GUI?

Por ejemplo, supóngase una aplicación que observa cierta tabla de una base de datos, mostrando su contenido mediante un JTable con su correspondiente TableModel. En el servidor se añade una nueva fila a la tabla y se notifica a la aplicación observadora. Esa notificación debería recibirse desde fuera del EDT. Habrá que actualizar el TableModel del JTable o asignarle uno nuevo dentro el EDT.

Más y más complicaciones


Al gestionar eventos de forma asíncrona se complica la gestión de eventos interrelacionados, como por ejemplo un MOUSE_PRESSED y el siguiente MOUSE_RELEASED. Al recibir la notificación del primer evento dentro del EDT y delegar su gestión a otro hilo, el EDT recibirá el siguiente evento, mientras es muy probable que aún se esté procesando el anterior: ¿y ahora, se delega el segundo evento a un tercer hilo, se descarta, se memoriza en algún sitio hasta que se pueda procesar?

También puede ocurrir que en medio de una tarea pesada sea necesario consultar al usuario, por ejemplo para que confirme si realizar o no una operación peligrosa, o para que introduzca algún dato necesario. Lo normal es que la tarea pesada se esté ejecutando fuera del EDT, por lo que habrá que pausarla para consultar al usuario dentro del EDT y luego continuarla fuera del EDT (leyendo la respuesta que se obtuvo dentro del EDT).

Aparte de la complicación de tener gestionar los eventos de forma asíncrona, al programar en varios hilos pueden aparecer los problemas intrínsecos de la programación concurrente: coordinar (sincronizar) los diversos hilos.

Otro problema añadido que aparece es el trocear el código del algoritmo creando objetos (típicamente anónimos) Runnable con el código a ejecutar dentro del EDT y con objetos SwingWorker (la opción típica) para ejecutar código fuera del EDT. En el caso de ser objetos de clases locales anónimas, además hay que tener en cuenta que no se puede acceder a variables locales del método envolvente a menos que dichas variables hayan sido declaradas como final.

Cuando se acopla fuertemente la vista con el modelo, se tiende a reducir código creándolos de manera conjunta. Por ejemplo, Swing lo hace con el constructor JTable(Object[][] rowData, Object[] columnNames), en el que se construye internamente un TableModel a partir de esos arrays. Inspirados en él, se podría tener la idea de crear una subclase con el constructor MyTable(Connection, Query) que realice una consulta sobre una base de datos y construya el JTable con las filas resultantes (o una función factoría JTable showQuery(Connection, Query) equivalente). El new MyTable se haría:
  • ¿dentro del EDT ejecutando una consulta a base de datos desde él, y por tanto, bloqueándolo?
  • ¿fuera del EDT violando la norma de manipular los objetos de GUI siempre dentro del EDT?
Ninguna de las 2 opciones es completamente buena.

Más vale prevenir


Resumiendo todo lo anterior, la gestión de los eventos se puede complicar mucho.

La cuestión es tener todo eso en cuenta, saber que todo eso está detrás para tener en cuenta que cuando algún evento pueda derivar en una tarea pesada (como entrada/salida) más vale entretenerse en orientarlo a un diseño asíncrono desde el principio, a tener que cambiarlo a posteriori.

Por experiencia sé que convertir código síncrono monohilo en código asíncrono multihilo con las restricciones comentadas en el artículo no es una tarea sencilla, y suele requerir modificar código en muchos más sitios de los que sería deseable.

Enlaces


Los enlaces de interés relacionados con esta serie de artículos sobre el EDT están aquí, en el primero de los artículos.


(Actualizado 30/04/2016)

jueves, 31 de marzo de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 1

Introducción


Si has programado Interfaces Gráficas de Usuario (GUI: Graphical User Interface) en Java Swing (JFC/Swing), deberías saber qué es el Event Dispatch Thread (de aquí en adelante EDT), pero tampoco me extrañaría que no hayas oído hablar de él.

Desarrollando una GUI en Java es típico preguntarse por qué durante el tratamiento de un evento (por ejemplo, la pulsación de un botón) pasan cosas como:
  1. que no aparece un cambio visual (texto, color...) que se trata de mostrar transitoriamente.
  2. que la interfaz se queda bloqueda/congelada/colgada (blocks/hungs/freezes).
La respuesta a ese tipo de preguntas se vuelve obvia cuando se conoce el modelo de hilos (hebras o threads) de ejecución que utiliza Swing.

El problema es que la mayoría de introducciones, tutoriales y guías rápidas de Swing no suelen hablar del EDT o no lo hacen entre los primeros puntos (en los tutoriales oficiales de JFC/Swing es el 4º y con el nombre que tiene, al menos a mí, no me dan ganas de leerlo para aprender a poner botones en pantalla), lo que suele provocar confusión en los programadores cuando empiezan a hacer cosas relativamente complejas.

Muchas de las cosas que comentaré sobre este tema se deben a mi experiencia usando Swing, y no a que yo tenga conocimientos de cómo funciona internamente.

¿Qué es y cómo funciona el Event Dispatch Thread?


El EDT es el hilo en el que la máquina virtual Java lleva a cabo todos las operaciones relacionadas con la GUI, como pintar componentes (objetos de la interfaz como botones, etiquetas, etc.) y atender los eventos de ratón y teclado (la exigua explicación del tutorial oficial aquí).

He subrayado pintar porque el proceso de pintar en pantalla también lo lleva a acabo el EDT, es decir, que en un instante determinado, o bien se está pintando en pantalla o bien se están atendiendo eventos, pero no ambas cosas a la vez.

Sabiendo eso, es muy fácil responder las preguntas anteriores:
  1. Al pulsar un botón no se puede mostrar el texto Cargando, realizar una carga lenta de datos y finalmente mostrar Cargado pensando que se llegará a ver el texto Cargando, porque el EDT no podrá repintar la pantalla hasta que finalice el evento completo, mostrando únicamente el resultado final Cargado pero no los posibles estados intermedios.
  2. Mientras el EDT esté atendiendo un evento, no podrá atender ningún otro ni refrescar la pantalla, por lo que si el evento tarda mucho tiempo el programa dará la sensación de estar colgado.
En esencia, el funcionamiento del EDT consiste en procesar, uno a uno y en orden de llegada, los eventos que han sido encolados en una cola (First In First Out) de eventos, considerando (re)pintar la pantalla también como un tipo especial de evento que se dispara cuando se alteran las propiedades gráficas (texto, colores, tamaño, etc.) de algún componente. Dicha cola especializada para gestionar los eventos de la GUI es la clase java.awt.EventQueue, que incluye el método isDispatchThread(), aunque es más común usar los métodos wrapper de la clase javax.swing.SwingUtilities, como isEventDispatchThread().

Pero es muy importante tener en cuenta que no es sólo el EDT quien puede encolar eventos, por lo que aunque el EDT esté bloqueado, pueden seguir llegando eventos que se procesaran posteriormente.

El procesamiento de cada evento incluye notificar a los oyentes correspondientes. Por tanto, cuando el programador registra un oyente de cierto tipo de evento, debe tener en cuenta que ese código se ejecutará en el EDT, y que si el tratamiento del evento es muy largo (por ejemplo, realiza alguna operación de entrada/salida), tendrá la GUI bloqueada hasta que el código del oyente termine, no pudiendo procesar nuevos eventos ni notificar a otros oyentes (lo que no impide que se sigan encolando eventos).

¿Por qué no se bloquea todo mientras el EDT está ocupado?


Aunque el EDT esté bloqueado, se pueden seguir haciendo algunas cosas con la GUI como cambiar el tamaño de las ventanas, minimizarlas y maximizarlas, mover el ratón y pulsar el teclado.

No hay que olvidar que la máquina virtual Java es una aplicación que se ejecuta sobre un sistema operativo real, y en el caso de necesitar interfaz gráfica, también sobre un gestor de ventanas (Windows, Gnome, KDE, etc).

Hay que tener en cuenta que hay eventos de bajo nivel, como el movimiento del ratón, las pulsaciones en el teclado y el manejo de las ventanas que son controlados por el gestor de ventanas y notificados a la máquina virtual Java como si ésta fuese un oyente. Luego Swing transformará algunos de esos eventos de bajo nivel en eventos de más alto nivel como que se ha hecho clic en un botón y los notificará a la aplicación.

Los eventos de bajo nivel se seguirán produciendo y aquello que sea controlado por el gestor de ventanas seguirá siendo posible, aunque la aplicación no recibirá todos esos eventos, pero en cualquier caso, los recibidos sólo se podrán procesar cuando el EDT se desbloquee, con lo que parecerá que se recibieron inmediatamente todos seguidos. Por ejemplo, no se recibirán todos los eventos de tipo MouseEvent que debería recibir un MouseMotionListener.

Aún así, probablemente pasarán efectos gráficos extraños con las ventanas, como que queden áreas sin pintar si se agranda.

Continuará


En futuros artículos continuaré hablando sobre el EDT y pondré un pequeño programa con el que ilustrar algunas de las cuestiones relacionadas con él que explique en los artículos.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 06/04/2016)

lunes, 29 de febrero de 2016

Subrutina recursiva en Fortran 77

Introducción


Cuando empecé los artículos previos sobre Recursividad Terminal, pensé en demostrarlos en Fortran 77 por ser el lenguaje por excelencia que no soporta recursividad. Sin embargo, encontré algunos problemas y opté por otros lenguajes. En este artículo he retomado ese reto. ¿Conseguí finalmente ejecutar una subrutina recursiva en Fortran 77?

Objetivo: implementar la función Factorial recursivamente en Fortran 77


Un ejemplo típico de función recursiva cuyo valor depende de invocarse a sí misma es la función matemática Factorial:

Factorial(N)     si N es 0 entonces 1//Caso base.
en otro caso, N * Factorial(N-1) //Llamada recursiva.

Utilicé dicha función como ejemplo por ser muy sencilla de implementar, muy ilustrativa y también porque yo no soy ningún experto en Fortran 77.

Problema 1: Fortran 77 no soporta recursividad


Al intentar crear un pequeño programa en Fortran 77 con una función (usaré el termino subrutina en general) recursiva me topé con el primer problema: no está permitida la recursividad ni directa (invocarse a sí mismo) ni indirecta (una subrutina no puede invocar a otra que ya esté en la pila de ejecución, es decir, que una misma subrutina sólo puede estar una única vez en memoria).

El compilador (supongo que el lenguaje en sí) que utilicé resuelve la imposibilidad de que una subrutina se invoque a sí misma o a otra que indirectamente pueda invocarla a ella permitiendo invocar únicamente subrutinas ya definidas (no se permiten declaraciones por adelantado como en otros lenguajes).

Así, al estar definiendo una subrutina, su cuerpo no puede tener invocaciones a sí misma porque aún no está completamente definida y obtendremos un error de subrutina (símbolo) desconocida. De igual modo, cada subrutina sólo puede invocar subrutinas que hayan sido previamente definidas, por lo que ninguna de ellas podrá contener invocaciones a ella.

Solución al problema 1


Siguiendo las indicaciones dadas por Andrew J. Miller en su página Example 1: Recursive Routines in FORTRAN 77 (and Fortran 90), utilicé las directivas EXTERNAL y CALL para conseguir que una subrutina se invoque a sí misma. Sólo sirve para subroutine, no sirve para function.

De este modo, la subrutina recursiva recibirá como argumento el nombre de una subrutina a la que invocar, que en este caso, será ella misma.

Problema 2: en Fortran 77 se pasan los argumentos por referencia


En Fortran 77 los argumentos siempre se pasan por referencia, por lo que hay que tener cuidado de no alterar el valor de aquellos argumentos que semánticamente sean de entrada, sobre todo si hicieran falta después de la invocación recursiva.

Solución al problema 2


En el caso de la función Factorial(N) es necesario realizar llamadas recursivas con el valor (N-1). Hay que tener en cuenta que si se altera el argumento con una asignación N= N-1 se está alterando el valor de la variable original, porque se pasa por referencia. Por lo tanto, hay que tener cuidado con el sitio donde se almacena el resultado de ese cálculo, ya que su ubicación será traspasada a la invocación recurisva.

Problema 3: subrutinas sin almacenar variables en la pila


En Fortran 77 el valor una una variable local se considera indefinido una vez que se sale de su ámbito. Sin embargo, es opcional, mediante la directiva SAVE, que se salvaguarde el valor de las variables locales de una subrutina entre una invocación y otra. En otras palabras, se solicita al compilador que almacene la variable local de la subrutina de forma estática.

Suena bastante chocante, pero después de todo, no es una idea tan disparatada dado que el lenguaje no permite que una misma subrutina esté dos veces en memoria, y por tanto, es irrelevante que sus variables locales estuviesen almacenadas estáticamente, ahorrando el coste computacional que conlleva mantener la pila de ejecución. Más discutible es que se programase usando esa característica como truco para reaprovechar el valor entre invocaciones.

Al parecer, ese comportamiento se hizo tan común que algunos compiladores antiguos (especialmente conocido el de DEC) se saltaban el estándar y siempre almacenaban las variables locales de forma estática sin que se pusiese explícitamente SAVE, y muchos programadores se acostumbraron a programar así.

El compilador que utilicé, el g77, tiene una opción para compilar en ese modo: -fno-automatic.

Al utilizar la directiva SAVE, la variable local V de la subrutina S ocupa siempre la misma posición de memoria M. En caso de que S se invocase recursivamente, se estaría sobreescribiendo constantemente la misma posición de memoria M, por lo que es muy probable que al acceder a V después de una invocación recursiva su valor difiera del valor que tenía antes de la invocación.

Solución al problema 3


Yo he forzado que aparezca este problema utilizando la directiva SAVE, demostrando que se puede eludir mediante una implementación basada en Recursividad Terminal, puesto que tras la invocación ya se tiene el resultado y no hay que realizar ninguna operación más (siendo por tanto irrelevante el valor de las variables locales en ese punto).

Demostración


Es muy importante tener en cuenta utilizar un compilador estricto para Fortran 77, ya que a partir de Fortran 90 ya no existen tantas restricciones (soporta recursión). Para las pruebas he utilizado:
Hoy día me costó encontrar compiladores de Fortran 77, así que realicé las pruebas sólo con ese compilador.

A continuación se puede ver el programa en Fortran 77 que escribí para este artículo, con una implementación recursiva de la función matemática Factorial. Se compone de la función factorial(num) que es un simple envoltorio (wrapper) para la subrutina recursiva factorial_inner(self, input, output), donde:
  • self es el nombre de la subrutina a invocar con CALL (ella misma en este caso).
  • input es el número cuyo Factorial se desea calcular (argumento de entrada).
  • output es la variable acumulador del resultado (argumento de entrada/salida).
Hay que prestar especial atención a las líneas 3, 10, 11 y 12.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
      subroutine factorial_inner(self, input, output)
          integer :: input, output, tmp
          save tmp
          external self

      
          if (input .GT. 0) then
c Con cualquier otro orden de estas 3 sentencias el programa
c fallará si se usa la directiva SAVE sobre la variable tmp.
              output= input * output
              tmp= input - 1
              call self(self, tmp, output)
          endif

          return
      end

      integer function factorial(num)
          integer :: num, output
          external factorial_inner

          output= 1
          call factorial_inner(factorial_inner, num, output)
          factorial= output

          return
      end


      program demo_factorial
      integer :: input, output
      integer :: factorial

      write (*,*) 'Input number:'
      read (*,*) input

      output= factorial(input)
      write (*,*) 'Factorial= ', output

      stop
      end

He puesto la variable tmp para evitar que el compilador cree variables temporales debidas a cálculos intermedios (expresiones) y para demostrar el funcionamiento de la directiva SAVE.

Si se elimina la directiva SAVE de la línea 3, el programa funcionará perfectamente aunque se reordenen las sentencias 10, 11 y 12 (con sentido claro), porque la variable tmp estará almacenada en la pila. Sin embargo, con dicha directiva activada, sólo fucionará correctamente en ese orden.

Obsérvese que, con la directiva SAVE activada, desde factorial_inner sólo se accede a la verdadera variable input (el argumento input de factorial_inner apunta al argumento num de factorial que a su vez apunta a la variable input del programa principal) en la primera invocación, de ahí en adelante se está trabajando con una referencia a la variable tmp (a la que se le ha asignado una posición fija en memoria), y por tanto, en el resto de invocaciones el argumento input y la variable local tmp apuntan a la misma posición de memoria. Si se intercambian las instrucciones 10 y 11, output estará mal calculado puesto que en la primera iteración input ya habrá sido decrementado. De hecho, ese intercambio de instrucciones provoca que el resultado siempre sea 0 porque al llegar a la invocación correspondiente al Factorial de 1 se realizaría la multiplicación output*input = output*0 = 0.

Conclusión


En respuesta a la pregunta que planteé al comienzo del artículo, sí ha sido posible implementar una subrutina recursiva en Fortran 77, con ciertas restricciones impuestas por el propio lenguaje, aunque lo más interesante ha sido aprender sobre lo útil que es la pila de ejecución y cómo la Recursividad Terminal puede ser de ayuda en ciertas condiciones.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 03/03/2016)

domingo, 31 de mayo de 2015

Recapitulando: año 2

Repasando los artículos del blog escritos a lo largo de este segundo año de existencia, puedes descubrir:


Si has leído alguno, muchas gracias y espero que te haya gustado o que te haya resultado útil.

Y recordar que los comentarios son bienvenidos.

Si quieres ver la recopilación del año anterior, sigue este enlace:



(Actualizado 31/05/2015)