Programación. Tema 1: Arrays (16/Mayo/2004) Apuntes elaborados por: Eduardo Quevedo,Raquel López Revisado por: Javier Miranda el 25 de Mayo de 2004 Tema 1: Arrays En este primer tema nos familiarizaremos con la programación mediante objetos con Ada. Para esto, a diferencia de la asignatura vista en el primer cuatrimestre, utilizaremos los paquetes Ada. En este tema utilizaremos arrays como estructura de datos básica para almacenar información en memoria del ordenador. Como ejemplo escribiremos un programa que gestiona una agenda, y comenzaremos considerando el caso más sencillo: presuponemos que no es necesario almacenar la información de forma ordenada. Tras ver el comportamiento que debe tener nuestra estructura de datos diseñaremos una interfaz que, en sucesivas revisiones, mejoraremos hasta obtener la funcionalidad deseada. Finalmente veremos una segunda versión de la agenda en que la información se almacena de forma ordenada, con lo que podemos utilizar algoritmos de busqueda que mejoran la eficiencia de nuestra estructura. Array no ordenado En el CD que tienes con material complementario de esta asignatura encontrarás un applet que muestra visualmente el comportamiento del array no ordenado. Conviene recordar que, por simplicidad, este applet solamente trabaja con números enteros, y que para utilizar un array como soporte de almacenamiento no sólo debemos fijar su tamaño máximo (para declarar el array) sino que también necesitaremos una variable entera que nos indique cuantos elementos hemos guardado en el array. Applet: Este applet nos permite insertar un nuevo elemento, buscar un elemento o borrar un elemento. Hay que tener en cuenta que cuando en un array se borra un elemento hay que recolocar todo su contenido, para así cerrar el espacio libre que se ha dejado al eliminar el número. Por ejemplo si en el siguiente array se borra el 45, quedará un hueco, eliminándose subiendo el 32 y el 23 6 33 45 32 23 6 33 32 23 6 33 32 23 1 Programación. Tema 1: Arrays (16/Mayo/2004) Interfaz: La programación mediante objetos utilizando el lenguaje de programación Ada se realiza mediante paquetes. Cada paquete consta de dos ficheros: la interfaz (fichero con extensión “ads”) y el cuerpo del paquete (fichero con extensión “adb”). En nuestro caso, las operaciones fundamentales que debemos poner en la interfaz son: - Insertar (Valor) - Existe (Valor) - Borrar (Valor) - Mostrar_Contenido La operación Mostrar_Contenido sólo se utilizará en la práctica como ayuda para comprobar el paquete, los tres primeros son fundamentales en cualquier base de datos, ya sea en arrays, árboles, pilas, listas, colas, etc. V 1.0: Interfaz de Array no ordenado Para hacer la interfaz en Ada utilizaremos un archivo con la extensión .ads. package Agenda is procedure Insertar (Valor : in Integer); function Existe (Valor : in Integer) return Boolean; procedure Borrar (Valor : in Integer); procedure Mostrar_Contenido; end Agenda; La implementación (el cuerpo) de este paquete sería así: package body Agenda is Contenido : array (1 .. 100) of Integer; Num_Elementos : Natural := 0; procedure Insertar (Valor: in Integer) begin Num_Elementos := Num_Elementos + 1; Contenido (Num_Elementos) := Valor; end Insertar; -- Resto de operaciones del paquete ... end Agenda; Como vemos, en el cuerpo estamos declarando un array de 100 enteros (Contenido) y una variable (Num_Elementos) que utilizaremos para recordar cuantos elementos hemos guardado en el array. Estas dos variables son globales al paquete ya que están declaradas fuera de los procedimientos. Esto significa que son comunes a todos los procedimientos (Insertar, borrar, etc.) y 2 Programación. Tema 1: Arrays (16/Mayo/2004) guardan la información hasta que se termine el programa (Si las hubiesemos declarado dentro de un procedimiento o función, desde que se llega al “end” del procedimiento las variables desaparecerian y no servirian para guardar información). Para usar el paquete desde un programa principal hacemos lo siguiente: with Agenda; procedure Ejemplo_Uso_1 is begin Agenda.Insertar (13); if Agenda.Existe (10) then Agenda.Borrar (10); end if; Agenda.Mostrar_Contenido; end Ejemplo_Uso_1; Los problemas que plantea esta versión son que sólo se puede trabajar con un array y que hay una limitación de elementos (en cuanto al tamaño de la tabla). Si no queremos escribir continuamente el prefijo del paquete (Agenda) Ada nos da la posibilidad de utilizar la sentencia “use” indicar que todas las declaraciones del paquete son immediatamente visibles. Esto significa que podemos utilizarlas “como si” estuviesen en nuestro programa. Por ejemplo, el siguiente programa es equivalente al anterior: with Agenda; use Agenda; procedure Ejemplo_Uso_2 is begin Insertar (13); if Existe (10) then Borrar (10); end if; Mostrar_Contenido; end Ejemplo_Uso_2; El principal inconveniente de esta implementación es que, como solamente existe una variable Contenido (la que esta declarada en el cuerpo del paquete), sólo se nos permite tener una agenda. Solución al problema de tener un único array Para que un paquete pueda gestionar multiples agendas debemos permitir al cliente del paquete (a quien hace el “with” de este paquete) que pueda declarar tantas agendas como quiera. La solución a este problema es declarar en la interfaz (que contiene todo lo que puede utilizar el cliente del paquete) un nuevo tipo de dato que tenga toda la información de la tabla. De esta forma, todo cliente (o usuario) del paquete puede crear tantas tablas como necesite. Como la información minima que necesitamos para gestionar cada agenda son dos variables (el numero de elementos y el contenido) tenemos que utilizar un 3 Programación. Tema 1: Arrays (16/Mayo/2004) registro (un record) para mantenerlas agrupadas. Por tanto, hacemos la siguiente modificación en la interfaz: package Agenda is type T_Contenido is array (1 .. 100) of Integer; -- Tenemos que declarar el tipo del array para poder utilizarlo -- a continuacion en la declaracion de campo Contenido de nuestro -- registro (record) type T_Objeto is record Contenido : T_Contenido; Num_Elementos : Natural := 0; end record; procedire Insertar ( . . . ); end Agenda; V 1.1: Interfaz de Array no ordenado Para que las declaraciones de tipos de datos que hemos puesto en la interfaz sean realmente utiles debemos modificar también la interfaz de todos los procedimientos y añadir un parámetro de tipo T_Objeto como primer parámetro. La interfaz queda entonces de la siguiente forma: package Agenda is type T_Contenido is array (1 .. 100) of Integer; type T_Objeto is record Contenido : T_Contenido; Num_Elementos : Natural := 0; end record; procedure Insertar (Objeto: in out T_Objeto; Valor : in Integer); function Existe (Objeto : in T_Objeto; Valor : in Integer) return Boolean; procedure Borrar (Objeto : in out T_Objeto; Valor : in Integer); procedure Mostrar_Contenido (Objeto : in T_Objeto); end Agenda; Veamos a continuación un ejemplo de uso de esta nueva interfaz. 4 Programación. Tema 1: Arrays (16/Mayo/2004) with Agenda; procedure Ejemplo_Uso is Mi_Agenda_1 : Agenda.T_Objeto; Mi_Agenda_2 : Agenda.T_Objeto; begin Agenda.Insertar (Agenda_1, 13); if Agenda.Existe (Agenda_2, 10) then Agenda.Borrar (Agenda_2, 10); end if; .Agenda.Mostrar_Contenido (Agenda_1); end Ejemplo_Uso; Aunque esta solución nos permite tener varias agendas, no nos permite indicar el tamaño de cada agenda (todas son de tamaño 100 porque la declaracion viene fijada en la interfaz del paquete para poder declarar el array). Solución al problema del tamaño de la Tabla Para que el usuario del paquete pueda especificar el tamaño de la tabla solamente tenemos que pasar un parámetro al record. Este parámetro se utiliza para declarar la variable contenido con el tamaño exacto que se necesite para cada variable. type T_Contenido is array (Positive range <>) of Integer; type T_Objeto (Maximo: Positive) is record Contenido : T_Contenido (1 .. Maximo); Num_Elementos : Natural := 0; end record V 1.2: Interfaz de Array no ordenado Incorporando esta nueva modificación en nuestra interfaz obtenemos: package Agenda is type T_Contenido is array (Positive range <>) of Integer; type T_Objeto (Maximo : Positive) is record Contenido : T_Contenido (1 .. Maximo); Num_Elementos : Natural := 0; end record; procedure Insertar (Objeto: in out T_Objeto; Valor : in Integer); function Existe (Objeto : in T_Objeto; Valor : in Integer) return Boolean; procedure Borrar (Objeto : in out T_Objeto; Valor : in Integer); procedure Mostrar_Contenido (Objeto : in T_Objeto); end Agenda; 5 Programación. Tema 1: Arrays (16/Mayo/2004) Fallo de esta interfaz El principal inconveniente de la interfaz que acabamos de diseñar es que el usuario puede (por error) modificar directamente los elementos del array sin utilizar los procedimientos del interfaz. Esto es posible porque la declaración del record esta completamente visible en la interfaz del paquete. Solución al posible mal uso de la interfaz Para resolver este problema Ada nos permite descomponer la interfaz en dos partes: la parte pública y la parte privada. En la parte pública ponemos todo lo que puede utilizar el cliente del paquete, y en la parte privada ponemos todas las declaraciones que hagan falta para que se pueda compilar la interfaz. V 1.3: Interfaz de Array no ordenado Si incorporamos declaraciones privadas en nuestra interfaz nos queda asi: package Agenda is -- Parte publica de la interfaz del paquete type T_Objeto (Maximo : Positive) is private; procedure Insertar (Objeto: in out T_Objeto; Valor : in Integer); function Existe (Objeto : in T_Objeto; Valor : in Integer) return Boolean; procedure Borrar (Objeto : in out T_Objeto; Valor : in Integer); procedure Mostrar_Contenido (Objeto : in T_Objeto); private -- Parte privada de la interfaz del paquete type T_Contenido is array (Positive range <>) of Integer; type T_Objeto (Maximo : Positive) is record Contenido : T_Contenido (1 .. Maximo); Num_Elementos : Natural := 0; end record; end Agenda; El compilador se encarga de que los clientes de este paquete (los clientes son los programas que hacen “with” de este paquete) solo puedan utilizar los nombres de los procedimientos y tipos de datos que estan en la parte publica del paquete, que son todas las declaraciones que hay entre la palabra is (en la linea 1) y la palabra private. Los clientes de este paquete 6 Programación. Tema 1: Arrays (16/Mayo/2004) Inconvenientes: El principal inconveniente de esta interfaz es que cuando la tabla está llena y se intenta insertar un elemento, el programa falla (Ada eleva la excepción Constraint_Error porque si el array esta lleno estariamos accediendo a una posición que esta fuera del array). De figual forma, cuando el array esta vacío y se intenta borrar un elemento que no existe el programa falla. Estos dos fallos se corrigen añadiendo excepciones en la interfaz de paquete, para que cuando ocurra una de las dos situaciones antes comentadas se notifique el error elevando la excepción necesaria. Es evidente que ni Existe, ni Mostrar_Contenido tendrán que elevar excepción alguna; el primero porque únicamente busca un determinado elemento y retorna un lógico y el segundo porque se limita a mostrar el contenido de un array. Por todo lo dicho sólo habrá que declarar dos excepciones: Lleno : exception; No_encontrado : exception; V 2: Interfaz de Array no ordenado Para incorporar las excepciones basta con añadir su declaración en la interfaz del paquete. Además, es importante documentar (mediante comentarios Ada) que excepciones eleva cada procedimiento, para que los clientes del paquete puedan tratar estas excepciones correctamente. package Agenda is Lleno : exception; No_encontrado : exception; type T_Objeto (Maximo : Positive) is private; procedure Insertar (Objeto: in out T_Objeto; Valor : in Integer); -- Excepcion: Lleno, cuando no hay mas espacio en el array function Existe (Objeto : in T_Objeto; Valor : in Integer) return Boolean; procedure Borrar (Objeto : in out T_Objeto; Valor : in Integer); -- Excepcion: No encontrado cuando la tabla está vacia procedure Mostrar_Contenido (Objeto : in T_Objeto); private type T_Contenido is array (positive range <>) of Integer; type T_Objeto (Maximo : Positive) is record Contenido : T_Contenido (1 .. Maximo); Num_Elementos : Natural := 0; end record; end Agenda; 7 Programación. Tema 1: Arrays (16/Mayo/2004) Inconveniente de la interfaz: El único inconveniente que le queda a esta interfaz es que sólamente trabaja con números enteros. En la primera práctica de la asignatura deberás escribir un paquete (la interfaz y el cuerpo) que trabaje con cualquier información. Esto se hace creando un registro T_Ficha que guarde toda la información que almacene nuestra agenda. Para que el programa sea realmente util deberemos utilizar uno de los campos de este registro como clave para realizar la inserción, busqueda o borrado de elementos. V 3: Interfaz de array no ordenado Si incorporamos todo lo anterior obtenemos la siguiente, y última, versión de nuestra interfaz: package Agenda is type T_Objeto (Maximo : Positive) is private; type T_Ficha is record Clave:.......; -- Aqui va el resto de campos de información de nuestra agenda end record; Lleno : exception; No_encontrado : exception; procedure Insertar (Objeto: in T_Objeto; Ficha : in T_Ficha); -- Excepcion: Lleno, cuando no hay mas espacio en el array function Existe (Objeto : in T_Objeto; Clave : in ...) return Boolean; procedure Borrar (Objeto : in out T_Objeto; Clave : in ...); -- Excepcion: No encontrado cuando la tabla está vacia procedure Buscar (Objeto : in out T_Objeto; Clave : in ...; Info : out T_Ficha); -- Excepcion: No encontrado procedure Mostrar_Contenido (Objeto : in T_Objeto); 8 Programación. Tema 1: Arrays (16/Mayo/2004) private type T_Contenido is array (positive range <>) of T_Ficha; type T_Objeto (Maximo : Positive) is record Contenido : T_Contenido (1 .. Maximo); Num_Elementos : Natural := 0; end record; end Agenda; Con respecto al cuerpo de esta interfaz veamos el cuerpo del procedimiento Insertar, en el que hay que elevar la excepción Lleno cuando la tabla esté llena y queramos insertar un elemento. Si el array no esta lleno lo que hay que hacer es añadir un elemento al número de elementos, y almacenar la ficha en el array utilizando como indice del array el nuevo valor del numero de elementos. De esta forma estamos realizando la inserción de los elementos siempre al final del array. -- INSERTAR -procedure Insertar (Objeto : in out T_Objeto; Ficha : in T_FichaI) is begin if Objeto.Num_Elementos = Objeto.Maximo then raise Lleno; end if; Objeto.Num_Elementos := Objeto.Num_Elementos + 1; Objeto.Contenido (Objeto.Num_Elementos) := Ficha; end Insertar; Verificación del programa: Test Pero, ¿cómo podemos saber si el programa que estamos haciendo funciona y lo hace bien?, la respuesta es sencilla, hacer un test que se comporte como un cliente de este paquete y verifique todos los posibles usos del paquete (tanto el uso correctos como el uso incorrectos). Como ejemplo, a continuación se exponen dos test que verifican si las excepciones están trabajando bien: 1. El primer test comprueba la función Existe. Para ello creamos una agenda en la que no insertamos nada (por tanto su array está completamente vacio) e intentamos buscar una ficha. Obviamente, si la función nos dice que encuentra la ficha claramente el paquete está funcionando mal y escribimos en pantalla la palabra “Fallo”; si no se encuentra es que el programa se ha portado bien (no encuentra la ficha porque aún no lo hemos insertado nada en la agenda) y el programa escribe en pantalla: Buscar en una tabla vacia ......................OK 9 Programación. Tema 1: Arrays (16/Mayo/2004) En caso de que la función Existe eleve cualquier excepción será también un fallo porque en la interfaz del paquete no se ha indicado que esta función eleve ninguna excepción: solamente debe devolver verdadero o falso. procedure Test_1 is Mi_Agenda : Agenda.T_Objeto (Maximo => 30); -- Esta agenda almacena como maximo 30 fichas. Mi_Ficha : Agenda.T_Ficha; begin Text_IO.Put ("Buscar en una tabla vacia ...................... "); -- Aqui debo rellenar la ficha con la informacion que quiero guardar -- en la agenda ... if Agenda.Existe (Mi_Agenda, Mi_Ficha) then Text_IO.Put_Line ("Fallo!"); else Text_IO.Put_Line ("OK"); end if; exception when others => Text_IO.Put_Line ("Fallo !!"); end Test_1; 10 Programación. Tema 1: Arrays (16/Mayo/2004) 2. El segundo test sirve para ver si el procedimiento Insertar está elevando la excepción Lleno cuando se llena la tabla. Para este test creamos una tabla en la que solo cabe un elemento, e insertamos dos fichas. Si al insertar la segunda ficha no se eleva la excepción, claramente el paquete no esta funcionando bien. procedure Test_2 is Mi_Agenda : Agenda.T_Objeto (Maximo => 1); Mi_Ficha : Agenda.T_Ficha; begin Text_IO.Put ("Insertar un objeto en una tabla llena........... "); -- Aqui debo rellenar la primera ficha con la informacion que quiero guardar -- en la agenda ... -- Añado la primera ficha a mi agenda Agenda.Insertar (Mi_Agenda, Mi_Ficha); begin -- Aqui debo rellenar la segunda ficha con la informacion que quiero guardar -- en la agenda ... -- Añado la segunda ficha a mi agenda Agenda.Insertar (Mi_Agenda, Mi_Ficha); -- Si no se ha elevalo la excepcion es que el paquete ha fallado Text_IO.Put_Line ("Fallo!"); exception when Agenda.Lleno => Text_IO.Put_Line ("OK"); when others => Text_IO.Put_Line ("Fallo!!"); end; exception when others => Text_IO.Put_Line ("Fallo (en la primera insercion)"); end Test_2; NOTA: Como puede verse en este ejemplo, para realizar este test se ha utilizado un segundo bloque “begin-end”. De esta forma aseguramos que el mensaje de fallo está asociado exactamente a la segunda llamada a Insertar (ya que podria darse el caso de que falle la primera llamada, en vez de la segunda, y si ponemos el manejador de excepciones en el procedimiento no sabriamos cual de las dos llamadas es la que elevó la excepción. 11 Programación. Tema 1: Arrays (16/Mayo/2004) Array ordenado La implementación anterior (insertando siempre la información al final del array) nos fuerza a recorrer todos los elementos del array cada vez que queremos realizar una búsqueda, ya que no hay forma de saber en que posición del array está el elemento. Una alternativa consiste en mantener ordenada la información que almacenamos en el array. Para realizar esta segunda versión podemos reutilizar la interfaz (el fichero con extensión “.ads”), los tests y el programa principal; solamente necesitamos modificar el cuerpo del paquete (el fichero con extensión “.adb). En concreto, debemos modificar el cuerpo de los procedimientos para realizar siempre la búsqueda mediante una función que realice la búsqueda binaria y que nos devuelva la posición donde encuentra el elemento; en caso de no encontrarlo la posición devuelta es exactamente la posición donde deberemos realizar la inserción. if Tabla.Num_Elementos = 0 then raise Tabla_Vacia; else -- Busqueda binaria loop Medio := Integer ( (Primero + Ultimo) / 2); if Tabla (Medio) = Clave then return Medio; elsif Primero = Ultimo then raise No_Encontrado; elsif Clave > Tabla (Medio) then Primero := Medio + 1; else Ultimo := Medio – 1; end loop; end if; 12 Programación. Tema 1: Arrays (16/Mayo/2004) Análisis: Para analizar la eficiencia de los algoritmos lo primero que se nos podría ocurrir es comparar los tiempos de ejecución de la versión mediante array no ordenado y la versión mediante array ordenado. La principal diferencias entre ambas es que la primera versión requiere realizar un bucle para encontrar la posición adecuada, mientras que en el array ordenado la busqueda solamente analiza unos pocos elementos para localizar la posición definitiva donde se debe realizar la inserción, ya que permite utilizar la búsqueda binaria. En el borrado ambas versiones tienen que hacer un bucle para cerrar el hueco. Intentemos llevar esta información a una tabla, para así facilitar la comparación de ambas versiones. Supongamos que la constante K representa la velocidad del ordenador: Inserción Búsqueda Borrado Array No Ordenado T=K T = K*N T = K*N Array Ordenado T = K*N T = K*log2(N) T = K*N En realidad, las tablas anteriores tienen información que no es en absoluto representativa. Si nos fijamos, todos los elementos de la tabla tienen la constante K, que lo único que hace es oscurecer la información realmente util. Para evitarla, la comunidad internacional ha definido el concepto de Orden de un algoritmo, en el que solamente se pone la información realmente relevante. Por ejemplo, si el tiempo de ejecución de un algoritmo es constante, decimos que es de orden 1, si al evaluar el tiempo de ejecución obtenemos un polinomio cuyo exponente de mayor grado es 3, entonces decimos que nuestro algoritmo es de orden N3: Por tanto, la tabla anterior quedaría asi: Inserción Búsqueda Borrado Array No Ordenado O (1) O (N) O (N) Array Ordenado O (N) O(log N) O (N) Una representación gráfica del orden sería la siguiente: 13 Programación. Tema 1: Arrays (16/Mayo/2004) Apéndice al tema 1: Curiosidades complementarias para la realización de las prácticas en el laboratorio (o en casa). Depurador: Para comenzar a usar el depurador hay que tener en cuenta los siguientes pasos: 1) Borrar del directorio todos los ficheros con extensión ali (*.ali). Los ficheros ALI (Ada Library Information) son ficheros que genera el compilador para establecer las dependencias entre los paquetes. Por tanto, siempre que queramos obligar al compilador a que compile de nuevo todos los paquetes simplemente tendremos que borrarle estos ficheros (al compilar, como el compilador no encuentra estos ficheros, se ve obligado a recompilar todos los paquetes). 2) Recompilar con la opción –g $ gnatmake –gnatc – g mi_agenda.adb 3) Arrancar el depurador $ gvd array_no_ordenado 4) Utilizar el depurador. Para el uso del depurador se deben de tener en cuenta los siguientes comandos básicos: - Run: Comienza a ejecutar el programa - Step: Siguiente sentencia (si la linea contiene la llamada a un procedimiento, fuerza el salto al comienzo del procedimiento, para comenzar a ejecutarlo) - Next: Siguiente linea (si la línea contiene la llamada a un procedimiento, se ejecuta todo el procedimiento y el depurador se detiene justo despues de completar la ejecución del procedimiento). Procedimientos de guardar y cargar: No tiene sentido una agenda que no guarde el contenido tras haber insertado una serie de elementos, para ello tienes que implementar dos procedimientos, uno que guarde el contenido de la agenda en un fichero y otro que lea el contenido del fichero y lo inserte en la agenda. Aunque puedes utilizar ficheros secuenciales o ficheros de acceso directo, como en ambos casos estas tratando con toda la información de la agenda, este es un claro ejemplo en que es recomendable utilizar un fichero secuencial. Obviamente, para añadir estos procedimientos tendras que modificar tanto la interfaz como el cuerpo del paquete. De igual forma también se recomienda añadir todos los procedimientos y funciones que quieras para completar tu práctica. 14