Revistas en papel
Anterior
MenĂº
Logotipo

Amiga.InFo Nº 0 - Noviembre/Diciembre 1994 - Programación Teórica

NOTA: Pulsando sobre las fotos con borde azul ampliarás la imagen.

La parte teórica
de la
programación

¿Te gustaría saber cómo se programa de forma teórica en la facultad de Informática? ¿Sí? pues estás de suerte, ya que Txinto Vaz nos introduce en este campo bastante desconocido...

Estamos en los 90. Atrás han quedado los 60, los 70, los 80. La industria informática ha adquirido una cierta madurez y las máquinas de que disponemos han adquirido una potencia considerable. Es evidente que los tiempos han cambiado y que las soluciones de antaño se nos quedan pequeñas. El software, como el resto de componentes del mundo informático, ha evolucionado adaptándose a las nuevas necesidades. Hoy en día es imprescindible para cualquier empresa disponer de un buen soporte informático, y que ese soporte FUNCIONE. No nos hemos de quitar de la cabeza que cualquier utilidad mínimamente seria, sobre todo las orientadas a ayudar a los demás programadores, como compiladores, debe de tener una seguridad de funcionamiento, que en algunos casos es, incluso, más importante que la propia eficiencia del programa en sí. En lenguaje de calle, hemos de intentar por todos los medios que el programa no "pete". Las consecuencias de una mala programación están a la vista de todos: los cuelgues del sistema, los gurus, los bugs de programas, etc. Hay fallos enormes en los programas actuales, ¿quién no se ha encontrado con uno? El usuario pensará que quizá no tiene tanta importancia la necesidad de asegurar el correcto funcionamiento de un programa, pero quizás unos cuantos ejemplos le hagan cambiar de opinión.

  • Ejemplo nº 1: Usted ha diseñado un programa que calcula una simple división y no tiene en cuenta que el usuario pueda insertar el valor 0 en el denominador, Evidentemente, el programa puede bloquear la máquina en ese caso, debido a que no es capaz de trabajar con números infinitos, y causará un desbordamiento de la variable. Quizá, si se ejecutase en un entorno como MSDOS, el programa no tendría mayores consecuencias: se reinicia la máquina y ya está. Pero... ¿y si el fallo se produce en su Amiga, que dispone de multitarea, cuando está ejecutándose Real 3D o Imagine y se encuentra a punto de terminar ese fotograma que ha tardado 19 horas en crear?.

  • Ejemplo nº 2: Imagínese que usted construye un programa para llevar la contabilidad de la empresa de un amigo o de un cliente y que, debido a un "bug" del programa, destruye los datos de 9 meses de trabajo. ¿Es importante o no una buena programación? Y llegando a los extremos más insospechados: ¿qué me diría de un "bug" de similares características en el ordenador que controla la central nuclear más cercana a su casa? En resumidas cuentas, la programación de ordenadores puede llegar a ser asunto de seguridad nacional. Además, hay que remarcar que los programas que sirven de plataforma a otras aplicaciones tienen una necesidad mucho mayor de fiabilidad, ya que los bugs se van propagando de programa en programa, y los fallos ocultos de un sistema operativo o un compilador pueden convertirse en fallos en la ejecución de programas creados para esas plataformas. ¿Se imaginan el programa de la central nuclear ejecutándose sobre Windows?


Diferentes tipos de errores en los programas

  • Errores de algorítmica: Son errores que comete el propio programador a la hora de diseñar el programa. Están causados por la "negligencia" del propio programador. El primer ejemplo del apartado anterior es un claro retrato de este tipo de error. El programador ha de prever los posibles usos del programa y gestionar bien todas las posibles entradas y salidas. Se supone que el usuario va a confiar en los resultados que la máquina le devuelva, con lo cual, en caso de prever que un caso pueda no resolverse satisfactoriamente, el programa ha de comunicar al usuario que esa entrada produce error, o no aceptarla, o comunicar de antemano al usuario que la haga: "El programa actual realiza la división entre numerador y denominador siempre que éste sea diferente de 0", o "Error: No se puede calcular el resultado", o "Error: Ha insertado 0 y no es una entrada correcta"... Estos artículos están dedicados a evitar este tipo de errores.

  • Error tipográfico: Error que se comete de forma involuntaria al teclear o introducir el programa al ordenador. El principal encargado de detectar estos errores es el compilador, aunque el propio lenguaje puede ayudarnos. Cuando el compilador se lo comunique con un mensaje de alerta o error. También tiene que ver el propio lenguaje en ello. Si en vez de una comparación metemos en un condicional una asignación, éste será más o menos detectable según el propio lenguaje que se esté usando. Esto se tratará en una próxima sección.

  • Otros errores: Pueden contarse entre ellos desde un corte de electricidad, una bajada de tensión, hasta un mal diseño de una placa del ordenador o del sistema operativo, etc... Este tipo de errores no pueden ser corregidos por el propio programador, pero es bueno que éste intente evaluar algunas de las posibles situaciones, como por ejemplo, las diferentes configuraciones que puede tener el ordenador sobre el que se va a ejecutar el programa, etc.


Elección de un lenguaje de programación

Hay lenguajes para todos los gustos, algunos de ellos principalmente orientados a aplicaciones específicas (como el Cobol, Fortran, etc...) y otros orientados a todo tipo de propósitos (Basic, C, pascal, Modula-2, etc...) Unos se dejan manejar más que otros y, aunque parezca mentira, los que menos se dejan manejar suelen ser precisamente los más indicados para una programación fiable. Una aplicación escrita en Ensamblador o en C es mucho menos fiable que otra diseñada en Modula-2 o Pascal. También es cierto que potencialmente, se pueden hacer más cosas en C que en Modula-2, e igual de fiables, pero para ello se necesita mucha más dedicación en lo que a corrección de errores se refiere. Este pequeño ejemplo lo ilustra claramente:

  • En C: If(!(sinerrores=TRUE)) {salir()}

El programador ha diseñado una función que cuando detecta que la variable booleana 'sinerrores' se encuentra a FALSE, sale de la función, y, por error tipográfico, en vez de '==' (igualdad booleana), ha escrito "=" (asignación). El programa asigna a 'sinerrores' un TRUE, que "machaca" el posible resultado que pudiera haber en la variable 'sinerrores', luego evalúa si salir es FALSO, y en NINGÜN caso es posible que se detecte, ya que siempre evalúa una variable que está inicializada a TRUE. Solución: el programa nunca saldría. El compilador de C debe detectar los casos que no estén contemplados en la gramática del lenguaje. En este lenguaje está permitido ésto y mucho más, como los famosos 'goto', que suelen ayudar a hacer ilegible el código. El compilador no detectaría error, y en las ejecuciones de prueba puede que tampoco detectáramos tal error, con lo cual ya tendríamos un "bug" vivito y coleando en nuestra "flamante" aplicación.

  • En Modula-2: Para empezar, el Modula, como el Oberon y otros lenguajes más formalizados, no te permite hacer una asignación dentro de una evaluación booleana, ni permite hacer 'goto', con lo cual no dejaría que nuestra aplicación se compilara sin generar un mensaje de error para que el programador se enterara de lo que ha sucedido.

Pero, como para todo en esta vida, en la programación no hay una piedra filosofal. Muchas de las rutinas gráficas de las magníficas 'demos' de los Amiga sólo pueden ser programadas en C o Ensamblador. Otras veces, no interesa tanto la fiabilidad como la velocidad de ejecución, como, por ejemplo, en un visualizador de animaciones. Otras veces es más necesario el ahorro de memoria que la fiabilidad, como, por ejemplo, en un juego para ordenadores con poca memoria. El código generado por lenguajes de alto nivel con una gran fiabilidad, como el Modula-2, suele ser más grande y lento que los generados en C, y, a veces, se necesita hacer "cosas raras" que el compilador no nos permite. Esta sección está orientada sobre todo a la programación fiable, con lo que será más fácil de implementar sobre Pascal o Modula-2 que sobre C o Ensamblador.


La necesidad de la caridad en el código

La claridad en el código es, si cabe, imprescindible para el programador de hoy en día. Un listado bien anotado, con comentarios y con unas tabulaciones bien ajustadas que permitan una correcta lectura del código, es esencial. Un programador pasa más tiempo mejorando aplicaciones que programando nuevas, e, incluso en ese caso, se suele recurrir a 'trozos' de anteriores programas o funciones que realizan acciones iguales o 'semejantes' a las que se necesitan. En un programa es tan importante su diseño como su mantenimiento. Un ejemplo claro de ello son los sistemas operativos actuales. Hay múltiples revisiones y mejoras del AmigaDOS, Finder, MSDOS, y lo mismo sucede con programas que tienen al menos un mínimo uso. Si la empresa en que se ejecuta un determinado software desea más prestaciones que las actuales, pero desea mantener el método de trabajo y las prestaciones existentes, se impone una revisión. Volvemos a lo de siempre: si el programa está bien diseñado, el nuevo programa será aún mejor y no tendrá problemas y si no, arrastrará problemas de una a otra versión hasta que no llegue a lo que se espera de él. Si el programador que lo codificó se encuentra dos años después con un listado críptico y laberíntico, con variables cuyo nombre no esté bien relacionado con su utilización, es más que probable que pierda gran cantidad de su tiempo en llegar a entender su propio listado para mejorarlo. Para ello es muy necesario que el texto esté bien estructurado y documentado, y que se guarden los algoritmos que se utilizaron en su diseño.


¿Qué es un algoritmo?

El algoritmo es, ni más ni menos, una expresión formalizada de lo que el programador tiene en mente. Es una representación de lo que se desea y lo que se obtiene, de sus entradas y salidas, de las variables usadas y un estudio de los posibles estados en l os que se puede encontrar un programa en su fase de ejecución. Normalmente, este algoritmo está escrito sobre papel, ya que es el modo más claro de entenderlo y consultarlo para el programador; es lo que diríamos un programa legible para una persona. El algoritmo no depende, normalmente, del lenguaje de programación utilizado. El algoritmo es el núcleo verdadero de un programa y es lo que el programador debe traducir a un lenguaje de programación para que la máquinapueda entenderlo. El algoritmo depende mucho de la persona que lo haya hecho, pero es muy recomendable utilizar algún sistema estándar para que más adelante podamos seguir entendiéndolo y que otras personas lo entiendan también.


Herramientas para la creación de programas correctos

La herramienta esencial para la creación de programas correctos es el dúo especificación-corrección. La idea en sí es bastante simple: un programador no sólo debe de crear un programa, sino que debe demostrar que éste funciona para todos los posibles casos. Se ha de programar de la misma manera en que un matemático formula sus métodos o resoluciones, mediante demostraciones.

Para reforzar esta afirmación diremos que, aproximadamente lo que el arquitecto o ingeniero es a la física. Éstos construyen sus proyectos a partir de elementos físicos de la misma forma que un programador recoge los elementos matemáticos para construir sus programas. La informática es el medio por excelencia para representar y utilizar estas estructuras, ya que trabaja con ideas, con información y con objetos no físicos. Hasta la llegada de los autómatas programables sólo se podían representar a través de la música, geometría, etc... que eran medios que estaban muy limitado por elementos físicos como las dimensiones.

Así pues, la mayoría de los expertos en programación de hoy en día han llegado a la conclusión de que el programador debe demostrar sus programas. Para ellos, se vale del método habitual: la lógica matemática. Los programas son ideas y ésta es la forma de evaluar las posibles interconexiones entre ellas.


La Lógica Matemática

Para quien no lo sepa, la lógica es la ciencia que nos ayuda a evaluar interconexiones entre unas ideas de las que partimos y que se suponen válidas. Esto suena complicado, pero la verdad es que la utilizamos inconscientemente:

"Juan me ha dicho que Pedro está con Luisa y Jesús me ha dicho que Luisa está en las Ramblas, luego Pedro debe estar en las Ramblas". En las frases anteriores tenemos dos premisas y una conclusión. Una premisa es un enunciado del que partimos y que se considera cierto:

"Pedro está con Luisa" y "Luisa está en las Ramblas" son las dos premisas de las que partimos y a partir de las cuales hemos de extraer una conclusión.

"Pedro está en las Ramblas" es una conclusión válida a la que llegamos tras operar incoscientemente la dos premisas anteriores con las leyes de la lógica.

La representación lógica de lo anterior sería ésta:

PL
LR
-----
PR

La lógica intenta evaluar la validez de los razonamientos, no de los conocimientos que se aportan en sus premisas. Este razonamiento o interconexión de ideas seguiría siendo válido independientemente de que las premisas "Pedro está con Luisa" o "Luisa está en las Ramblas" sean o no ciertas. La lógica no está en absoluto ligada a las premisas que se formulan y de las que se parte. La siguiente frase sería evaluada con una interconexión válida entre ideas: "Sócrates era griego" ( SG ), los griegos eran verdes ( GV ), luego "Sócrates era verde" ( SV ). La forma de representar ésto sería:

SG
GV
-----
SV

Para aquellos que todavía encuentren extraña esta última afirmación, les diremos que también utilizamos este concepto inconscientemente. Si no, piensen que, seguramente, más de una vez han pronunciado frases del tipo. "Yo no te mentí, a mí me dijeron que Pedro estaba con Luisa, y como Luisa estaba en las Ramblas, supuse que él estaría allí." Nuestro pensamiento en sí sería válido, aunque nuestro informador nos haya mentido, y como lo que la lógica evalúa es la validez de los razonamientos, éste sería dado por válido.

Las diferentes operaciones que se suelen encontrar en lógica son:

  • pq Condicional "Si p entonces q"

    "Si es Sócrates, entonces es griego"

  • pq Conjunción "p y q"

    "Sócrates es griego y Sócrates es verde"

  • p v q Disyunción "p y q"

    "Sócrates es griego y Sócrates es verde"

  • p Negación "no p"

    "Sócrates no es griego"

Hay otras operaciones lógicas como las variantes de éstas (disyunción negada, conjunción negada, etc...), o como los signos (para todo) y (existe).

Vamos a dar un último ejemplo de la potencia de la algorítmica metódica, aunque ésta no sea su sección. Hemos dicho que mediante ella podemos crear programas que podamos especificar mediante operaciones lógicas. ¿Recuerdan las definiciones matemáticas del límite o las derivadas, todas ellas llenas de símbolos como el o el ?
¿Cómo lo implementarían?
Estas operaciones lógicas son las mismas que se encuentran en los condicionales de cada uno de los lenguajes de programación que tenemos al alcance, por lo cual, aprenderlas no es ninguna carga extra para cualquier persona que vaya a programar.

Más adelante daremos repasos de lógica más avanzada. De momento, y para no tener que rebuscar en libros, las tablas de las operaciones booleanas elementales son estas:

(V=verdad y F=falso)

a b ab a v b a -> b a
F F F F ? V
F V F V ? V
V F F V F F
V V V V ? F


Discusión sobre el condicional lógico

A estas alturas ya se habrá preguntado a qué se deben los interrogantes en la expresión del condicional. El condicional o "implica" sólo relaciona dos premisas con una conexión muy débil. 'a' "implica" 'b' (a->b) nos dice que en caso de que 'a' sea verdadero, 'b' ha de serlo también. No nos dice nada de los casos en que 'a' sea falso (casos primero y segundo). En caso en que 'a' sea verdadero, y 'b' falso, se observa que contradice la definición del "implica", es decir, 'a' es verdadero, pero no es falso. Éste es el único caso en el que se puede opinar sobre la veracidad o no del enunciado. En el caso que resta no podemos decir nada. Como pequeño ejercicio mental, determínelo usted mismo. Como ayuda, piense en lo siguiente: "Sócrates es europeo y es griego, pero eso no quiere decir que el hecho de que Sócrates sea europeo implique que sea griego"


Especificación

La especificación consiste en usar la lógica para expresar el estado lógico de salida en un programa y el estado lógico en el que debe finalizar el mismo, de forma que se nos garantice que el programa es correcto. En términos mundanos, lo que tenemos y lo que queremos, de lo que partimos y a lo que llegamos, en un algoritmo.

Nunca dos programas que realicen tareas diferentes pueden tener la misma especificación, y nunca una especificación puede dar lugar, salvo por error, a dos programas diferentes. Por ejemplo, un programa que calcule una división de dos números tendría una especificación determinada, y otro que intercambie dos variables, otra. Aquí presentamos un esquema un tanto informal de los pasos que sigue la mente de un programador antes de la realización de la especificación.


Ideas previas a la realización de un programa

Idea de la que partimos: Tenemos un numerador entero cualquiera 'x' y un denominador entero diferente a 0 llamado 'y'.

Idea a la que llegamos: Queremos un entero 'c' que sea el cociente de la división 'x DIV y' y otro 'r' que sea el resto.

Evolución de la idea: 'c' ha de ser tal que 'y*c+r=x' y a su vez, 'y*(c+1)' ha de ser mayor que 'x', y 'r' ha de ser menos que 'y' (definición matemática de la división entera).

Especificación:
Precondición: Expresión lógica de lo que tenemos al comenzar la ejecución del programa:
                     { x e y son Enteros, y!=0}
Postcondición: Expresión lógica de lo que tenemos al acabar el programa:
{c y r son Enteros, y*c+r=x, y*(c+1)>x,r<y}

Ejemplo sencillísimo: Una función que intercambie el valor de x e y. El programa sería algo así:
Función cambio (entradas: x,y son enteros salida: x,y son enteros)
{Precondición: x e y son Enteros, x = X, y = Y}
VAR: comodin es entero;
comodin:=x;
x:=y
y:=comodin;
{ Postcondición: x e y son Enteros, y = X, x = Y}
retorna x,y;
finfuncion

El programa de la división de enteros quedaría así:

Función DIV (ent: x,y enteros sal:c,r enteros)
(* Función para calcular una división entera *)

{P: xe y son Enteros, y!=0}
c:=0;
r:=x;
mientras (r>=c) hacer
c:=c+1;
(* Vamos incrementando el cociente hasta llegar al deseado*)
r:=r-c; (* Decrementamos el resto hasta que éste sea menor que el dividendo*)
mientras
{ Postcondición: c y r son Enteros, y*c+r=x, y*(c+1)>x,r<y}
retorna c,r;
finfuncion

Para que se vaya familiarizando con la especificación, he aquí la precondición y postcondición de otra función:
- Máximo de dos números:
{Pre: x e y son Enteros }
{Post: (x=>y)->(M=x),(y=>x)->(M=y)}
Pregunta: ¿Qué valor toma si x=y?
Respuesta aplastante: Da igual, ya que x=y.


La demostración

La demostración consiste en la verificación paso a paso de una función mediante el análisis de sus estados iniciales, final e intermedios, y el estudio de cada una de las transformaciones de estos estados partiendo desde el inicial, pasando por los intermedios, y llegando al final.

En términos más comunes, consistiría en demostrar que el programa funciona perfectamente verificando todos y cada uno de los estados intermedios. La Precondición y la Postcondición son los estados inicial y final. Saltando deuno a otro se puede demostrar que el algoritmo es correcto. A la precondición la llamaremos P, a la postcondición Q, y a los pasos intermedios P1, P2, P3, P4, etc. Para empezar, un ejemplo sencillísimo, el del cambio:

Función cambio (ent: x,y son enteros sal:x,y son enteros)
{P: x e y son Enteros, x = X, y = Y}
VAR: comodin es entero;
comodin:=x;
{P1: comodin es Entero, comodin =x}
x:=y;
{P2: x e y son Enteros, x = Y, y = Y, comodin es Entero, comodin = x}
y:=comodin;
{P3: x e y son Enteros, x = Y, y = X, comodin es Entero, comodin = x}
{Q: x e y son Enteros, y = X, x = Y}
retorna x,y;
finfunción

En el ejemplo se va viendo cómo se modifica la Precondición mediante las asignaciones hasta llegar a la Postcondición y queda demostrado realmente que el programa funciona.

OBSERVACIÓN FINAL: A simple vista, el desarrollo de funciones o programas verificados parece un poco tedioso, pero la verdad es que, con la práctica, es el mejor método de desarrollo de funciones. Si el lector observa con atención las Postcondiciones, se dará cuenta de que no son sino las definiciones o fórmulas matemáticas que nos encontramos en los libros de cálculo, física, química, etc. Al principio, no negamos que es mucho más rápido programar de la forma tradicional, pero esto tiene sus inconvenientes, como, por ejemplo, que está muy limitada por la "genialidad" del programador. Ha sido muy demostrado en los libros de programación que el programar "chapuza" vale para las cosas pequeñas y fáciles, pero se constata el hecho de que, ante una función complicada, el programador no tiene conocimientos ni sistemas fiables para "atacarla", sobre todo si es una función recursiva (que se llama a sí misma). En la programación metódica se dan herramientas para que, siguiendo paso a paso mecánicamente, y partiendo de una fórmula matemática, se llegue siempre a un programa, más o menos efectivo y correcto que satisfaga las expectativas. Da, a su vez, mecanismos para transformas cualquier función recursiva (mucho más sencillas de lo que parecen), a la función iterativa (bucle) más optimizada que se puede encontrar, y que sea de demostrada corrección, etc... Ya no se depende de que no se tenga un buen día ni de que no se sea un genio, la función sale sin problemas y, con la práctica, sin esfuerzo.

Además, para remarcar más los méritos de una programación correcta, existen métodos para "derivar" una función, es decir, para "coger" una especificación y de ella "sacar", mediante unos pasos siempre mecánicos e idénticos, una función que ya queda demostrada automáticamente.

Pero esto se verá en próximos artículos. En el siguiente hablaremos de la notación que ya has estado usando en este capítulo, y mostraremos métodos de corregir los diferentes bucles condiciones, etc...

Incluiremos códigos fuentes en distintos lenguajes, como Modula-2, Pascal y C, de los ejemplos que vayan introduciendose en el curso.


Envía esta página web a un amigo:
Esta opción está desactivada temporalmente, rogamos disculpen las molestias

Volver a la página anterior

Al menú principal