sábado, 5 de febrero de 2022

Introducción al flujo de ejecución y estructura de los programas.

 En el principio, era la nada.

Mucho tiempo después, cuando se inventó el proceso de programar máquinas de cálculo general se estableció la primera estructura de los programas. Esa primera aproximación se le conoce coloquialmente (y seguramente tiene un nombre más formal) como "caída de piedra". Tal cual, las instrucciones del programa se van ejecutando desde el inicio hasta el final, una a la vez, hasta llegar al final del programa. Este mecanismo de ejecución subyace hasta nuestros días y el olvidarnos de eso suele causar algunos de los errores más frecuentes de los programadores novatos. Después, con la creciente complejidad de los programas, surgió un nuevo paradigma de organización que se conoce como "programación estructurada", que nos permite fragmentar nuestros programas en secciones más pequeñas, idealmente aisladas en unidades funcionales ("funciones" les decimos) que determinan fragmentos de código ejecutable; en el fondo, la programación estructurada nos permite "aventar" la piedra a caer en diferentes secciones del programa, y nos proporciona mecanismos para obtener resultados de esos "saltos" en la ejecución. Después, cuando las unidades funcionales se volvieron cada vez más complejas, surgió el paradigma de "orientación a objetos", donde la programación pasa a centrarse en la definición de "clases" que no son otra cosa mas que machotes de ejecución, con sus valores, unidades funcionales, y sus reacciones aisladas y bien definidas, que una vez más, vuelven a la caída de piedra; con la orientación a objetos surge también el concepto de "Excepción", que se puede traducir algo libremente como "error". A continuación vamos a analizar a detalle cada uno de los tres paradigmas.

"Caída de piedra"


Es quizás la aproximación más simple que puede hacer a la ejecución de un programa, y equivale a tener una lista de instrucciones enumerada. Es tan fácil como pensar en:

  1. Inicia la ejecución.
  2. Declara una variable.
  3. Asigna el valor "hola, mundo" a esa variable.
  4. Vuelca el valor de esa variable a la salida estándar.
  5. Termina la ejecución.

Las cinco instrucciones serán ejecutadas desde la 1 a la 5, manteniendo un ámbito acumulativo, es decir, que no se puede usar una variable antes de ser declarada. Dentro de este paradigma se consideran acciones de modificación del flujo, como por ejemplo condicionales ("if" o "switch"), o ciclos ("for", "while"), sin embargo, en algún momento después de estas estructuras (y dentro de ellas) se volverá a la ejecución secuencial hasta el final del programa (aunque todos sabemos que se podría encerrar esa ejecución en un ciclo infinito, que esto volverá a mencionarse).

Para programas simples, y en el fondo, todo los lenguajes de programación mantienen esta estructura, pues todas las funciones siguen ejecutándose de manera secuencial en última instancia.

Programación estructurada.


En algún punto los problemas a resolver se fueron complicando cada vez más, y los programas se fueron haciendo cada vez más largos. Unas cien líneas de código pueden ser fácilmente leídas y entendidas por un programador promedio, pero arriba de las 300 líneas se pueden volver prácticamente imposibles de analizar. La solución fue dividir el programa en secciones de funcionalidad específica, llamadas "funciones" (y a veces subrutinas), y esas funciones, entonces, son invocadas una a una; la lectura se simplifica al volverse más expresiva al humano, a la par que surge el concepto de reutilización de código, es decir, que una función puede ser invocada en reiteradas ocasiones.

Aquí entra en juego dos tipos de funciones: las que requieren regresar un valor al punto donde fueron invocadas, y aquellas que no lo hacen. Algunos lenguajes, sobre todo más viejos, hacen una diferencia explícita entre ambas: si no regresan un valor, se les llama "subrutina" o "procedimiento", mientras que las que regresan valores, se les llama "funciones" simplemente.

Un punto adicional a mencionar, es que cuando una función regresa un valor, en ese momento se interrumpe la ejecución de la misma, por lo que el resto de las líneas de código después de la instrucción "return" serán ignoradas.

El estándar POSIX considera, así mismo, que un programa "regresa" un valor al terminar su ejecución, por lo que es importante dejar un "return 0;" al final de las funciones "main()" para indicar al sistema operativo que la ejecución del proceso fue exitosa (también, por eso, en C y C++ la función "main()" es declarada como "int"). Otros valores serán leídos como códigos de error por el sistema operativo.

Programación orientada a objetos.

El tema es bastante amplio y en este texto nos concentraremos en las particularidades del flujo de ejecución.

Cuando se habla de un programa orientado a objetos debemos pensar en una extensión de la programación estructurada, permitiendo una mayor reutilización de código, niveles más altos de abstracción y en algún punto, hacer más realista el modelado de los problemas.

Se basa en "clases", una clase será la definición de una estructura de código que busca agrupar variables, procedimientos y referencias necesarias para realizar una tarea específica. Generalmente, esas clases serán utilizadas a través de sus instancias, que son conocidos como "objetos". Un punto a tener en cuenta es que la gran mayoría de los lenguajes de programación no permiten modificar la definición misma de un objeto, sino que el programador trabaja a partir de la "clase". Javascript es una salvedad, que permite la modificación dinámica de los objetos, que además, no persistirá en subsecuentes ejecuciones del mismo código.

Cuando hablamos de clases, surge un nuevo concepto que es el de "ciclo de vida" de un objeto: como tal entenderemos la secuencia de eventos de creación (generación de la instancia), preparación, vida útil y destrucción (finalización de la instancia). Las clases que gestionan la funcionalidad de formas o ventanas (Java, C++ con Qt, etc.) suelen especificar en su documentación eventos adicionales en su ciclo de vida, que pueden incluir la minimización, el cerrado, el redimensionado, etc.

Un concepto surge con este paradigma de trabajo es el de "excepción", que puede ser un error ocurrido durante la ejecución de un código. En el caso de C++, sin embargo, los errores suelen ser generados como señales de sistema operativo, y solamente cuando una clase explícitamente lo genera, excepciones. Otros lenguajes suelen, en cambio, suelen disparar excepciones para cualquier error. Una excepción suele ser una instancia de un objeto que hereda directamente la clase abstracta "Exception" (Java, sobre todo).


Existe, además, un mecanismo específico para evitar que una excepción interrumpa la ejecución global del código, y ese mecanismo son los bloques "try" y "catch"; esa estructura está especialmente diseñada para contener secciones de código que sospechamos que pueden generar un error. Si el error llegase a ocurrir, se interrumpe la ejecución del código en el punto donde se dispara, y el control de ejecución pasa al bloque "catch", que pueden configurarse para recibir diferentes tipos de excepciones; una vez que se concluye el bloque "catch" se continua ejecutando el código hasta el final de la función o procedimiento.


Se considera que el mecanismo de manejo de excepciones como demasiado costoso, prefiriéndose siempre evitar la generación de las mismas. Si una excepción no cae dentro de un bloque "try" dentro de la actual función, "burbujeará" a la siguiente función en la pila de llamadas, y si no existe ningún bloque "try" en la pila de llamadas, el programa entero interrumpirá su ejecución reportando un error al sistema operativo.

Extra: La pila de llamadas.

Tanto en programación estructurada, como en orientación a objetos, existe un control de la secuencia en que se han invocado las funciones. En inglés se le conoce como el "stack". La pila de llamadas suele mostrar funciones fuera de nuestro control, relacionadas más bien con la infraestructura del lenguaje que estamos trabajando. No es para espantarse, pero es importante poder detectar qué partes de esa pila podemos controlar, resultando bastante útil a la hora de depurar un código.

Conclusiones.

Aún el lenguaje de programación más moderno sigue manejando internamente el viejo "caída de piedra", pero suelen dar la ilusión de otras maneras. Generalmente, un proceso de Java, por ejemplo, ejecutará un programa muy simple que generará una instancia de la clase principal de la aplicación y pasará el control de ejecución a un ciclo infinito donde se ejecutará el hilo de eventos de esa clase... pero en el fondo, seguirá siendo una secuencia lineal.

No hay comentarios.:

Publicar un comentario