Universidad Simón Bolívar Departamento de Computación y Tecnología de la Información CI3641 Lenguajes de Programación I Guía Corta: Iteradores Esta guía presenta algunos conceptos básicos y ejemplos de iteradores. Nota: Es importante recordar que esta guía es un complemento, cuyo objetivo es ayudar al entendimiento de estos conceptos y su aplicación en la práctica. No debe considerarse un reemplazo para la bibliografía ocial del curso. 1. Preliminares: Repetición Determinada e Indeterminada Los lenguajes imperativos ofrecen una variedad de instrucciones de repetición, en donde se puede ejecutar una instrucción dada repetidamente. La cantidad de iteraciones que se ejecutarán dependerán de algún criterio asociado a la repetición. De este criterio surge una clasicación que divide estas instrucciones de repetición en dos grandes categorías: Repetición determinada: Es una instrucción de repetición en donde la cantidad de ite- raciones es conocida desde el momento en que se empieza a ejecutar el ciclo. Por lo general tienen una sintaxis similar a la siguiente (si bien las palabras especícas pueden cambiar): for hxi from hbegini to hendi with hstepi do hinstri Este tipo de instrucción asigna a la variable luego ejecuta la instrucción instr x el valor de evaluar la expresión en y suma el valor de evaluar menor que el resultado de evaluar end, step. begin, x es Si el nuevo valor de se realiza una nueva iteración. De lo contrario, la repetición termina. Nótese que se puede calcular la cantidad de iteraciones que hará una instrucción de este estilo: #iteraciones = end − begin step (1) x no puede ser begin, end y step Para que este tipo de iteración sea verdaderamente determinada, la variable alterada en el cuerpo de la instrucción instr. Así mismo, las expresiones deberán evaluarse una sola vez, al inicio de la repetición. Existen otros tipos de repeticiones determinadas interesantes, como el método Ruby. Este último se aplica a un entero a ser ejecutado n n times de y toma como argumento un bloque de código veces. Este tipo de iteración es un caso especial de un tipo de iteración determinada sumamente útil: los iteradores, los cuales trataremos en detalle más adelante. Repetición indeterminada: Es una instrucción de repetición igualmente, salvo que la cantidad de iteraciones no está determinada al iniciar la ejecución del ciclo. Existen iteraciones indeterminadas en varios sabores, sin embargo las más comunes tienen una sintaxis similar a la siguiente (si bien las palabras especícas pueden cambiar): while hguardi do hinstri 1 Este tipo de instrucción primero comprueba que la evaluación de la expresión booleana guard sea true. En tal caso, ejecuta la instrucción instr y vuelve a indagar sobre el guardia de la misma manera. En caso contrario, la repetición termina. Existen otros tipos de iteraciones indeterminadas con comportamientos similares, entre ellos: dowhile, repeat, loop, until, etc. Incluso existen instrucciones determinadas dis- frazadas de instrucciones determinadas, como el for for (hinstrpre i ; hguardi ; hinstrpost i) { hinstri; } 2. en lenguajes como C y Java. ≡ hinstrpre i; while (hguardi) { hinstri; hinstrpost i; } (2) Iteradores Los iteradores son un caso especial de instrucciones de iteración determinada en el que se itera sobre los elementos de una colección. Por lo general, se comportan utilizan por medio de funciones o métodos aplicados a la colección deseada, aunque algunos lenguajes tienen iteradores por defecto para las colecciones básicas. En Python, el siguiente programa suma todos los elementos de la lista lista y lo imprime: x = 0 for elem in lista: x = x + elem print x En Ruby, el siguiente programa imprime todos los números del 0 al 41: 42.times {|x| print x, ' '} En el primer caso se usó el iterador por defecto para listas de Python. En el segundo caso se utilizó el iterador 2.1. times para enteros que viene como estándar en Ruby. Iteradores verdaderos y simulados Los iteradores lucen a primera vista como funciones, salvo que en vez de devolver valores a través de return, lo hacen a través de yield. La diferencia fundamental entre ambas construcciones es la siguiente: Al encontrar un return el control de ujo vuelve al punto de invocación, devolviendo el valor especicado. Al volver a ejecutar la función, la misma empieza su ejecución desde cero. 2 Al encontrar un yield el control de ujo vuelve al punto de invocación, devolviendo el valor especicado. Al volver a ejecutar la función, la misma empieza su ejecución desde la instrucción siguiente al yield. A esta clase de iteradores se les conoce como iteradores verdaderos y son usuales en lenguajes de programación imperativos interpretados, ya que su implementación implica una administración astuta para mover marcos de la pila al heap y de vuelta. Lenguajes de programación compilados que desean ofrecer la ventaja de los iteradores, usualmente los ofrecen por medio de librerías y algún estado mutable asociado a las mismas. Por ejemplo, en Java se puede ofrecer un iterador para cualquier objeto haciendo que el mismo imple- iterator(). A su vez, debe implementarse un objeto Iterator que incluya implementaciones de next() y hasNext(). A este tipo de iteradores mente la interfaz Iterable, se les conoce como que pide una fución iteradores simulados. Los iteradores, tanto verdaderos como simulados, están diseñados para usarse en repeticiones determinadas y aunque pueden invocarse en otros contextos, la utilidad fuera de repeticiones es limitada. 2.2. El camino del bien Si se quiere ejecutar un iterador verdadero a mano, a veces los cálculos pueden ser engorrosos y complicados. Por lo tanto, se propone un método que permite ejecutar estos iteradores de manera organizada y meticulosa. Los pasos a seguir son los siguientes: 1. Enumerar cada linea de código ejecutable, de forma que se pueda hacer referencia a ella fácilmente. 2. Cada nivel de invocación de un iterador tendrá su propio marco de pila con las variables involucradas que incluye un pc (program counter ). 3. Al invocar un iterador: Si no existe ejecución en dicho marco (la la del pc está vacía), comenzar la ejecución en la primera línea ejecutable de la denición del iterador. Si ya existe ejecución en dicho marco (la la del pc contiene valores), comenzar la eje- cución desde la línea ejecutable que sea lógicamente siguiente a la última especicada en pc. 4. Cada linea de código que se ejecuta debe ser marcada en la la de 5. Si la ejecución encuentra un de pila actual. pc. yield, devolver el control al invocador sin desempilar el marco 6. Si la invocación al iterador está asociada a un ciclo: Si el iterador devuelve un valor, se asocia a la variable correspondiente y se ejecuta el cuerpo del ciclo. Si el iterador no devuelve nada, se termina la ejecución de dicho ciclo. 3 7. Si se llega al nal de la denición del iterador se devuelve el control de ujo al invocador, pero sin ningún valor asociado. Como ejemplo, consideremos el siguiente iterador en pseudocódigo que calcula, dada una lista de enteros, devuelve listas de enteros como resultado. (0) (1) (2) (3) (4) iterator misterio (lista : [int]) -> [int] begin if (lista = []) yield [] else for p in misterio(lista.cola) yield p yield [lista.cabeza] ++ p end end end Consideremos también el siguiente código que hace use de ese iterador: for p in misterio([1,2,3]) print p Apliquemos entonces el camino del bien para ejecutar el programa y averiguar lo que produce. Al iniciar la ejecución la pila se verá así: pc lista p La comparación lista = 2 0 [1, 2, 3] - es falsa por lo que se mueve el pc a la línea dos y se ejecuta el iterador que aparece ahí. La pila se verá así ahora: pc lista p pc lista p 0 [2, 3] 0 2 [1, 2, 3] - Si se continúa así, se llegará eventualmente a una invocación en la cual la lista si es vacía. En ese punto, la pila se verá así: 4 pc lista p pc lista p pc lista p pc lista p 0 [] 0 2 [3] 0 2 [2, 3] 0 2 [1, 2, 3] - En este punto, la ejecución avanza por la instrucción (1) y se ejecuta el yield, devolviendo el primer valor al invocador, asociando la p a dicho valor y avanzando la ejecución a la instrucción (3). La pila se verá así entonces: pc lista p pc lista p pc lista p pc lista p 0 1 [] 0 2 3 [3] [] 0 2 [2, 3] 0 2 [1, 2, 3] - En la instrucción (3) simplemente se devuelve lo que valga p con otro yield. Así mismo ocurrirá hasta llegar al nivel global en el que lo primero que se imprimirá será []. La pila se verá así en este momento: pc lista p pc lista p pc lista p pc lista p 0 1 [] 0 2 3 [3] [] 0 2 3 [2, 3] [] 0 2 3 [1, 2, 3] [] 5 Imprime: [] Al continuar ejecutando la repetición, se invoca nuevamente al iterador (el cual comenzará desde la instrucción siguiente a donde terminó). La instrucción (4) devolverá la cabeza de la lista (que en este caso es 1) concatenado a p (que en este caso es []). La pila después de esto se verá así: pc lista p pc lista p pc lista p pc lista p 0 1 [] 0 2 3 [3] [] 0 2 3 [2, 3] [] 0 2 3 4 [1, 2, 3] [] Imprime: [] [1] Al volver a invocar el iterador, el mismo avanza de la instrucción (4) a la instrucción (2) nuevamente y el proceso es análogo. Sin embargo, al ejecutar todas las instrucciones (4) de los diferentes niveles de ejecución, la pila se verá así: pc lista p pc lista p pc lista p pc lista p Imprime: 0 1 [] 0 2 3 4 2 [3] [] 0 2 3 4 2 3 4 2 [2, 3] [] [3] 0 2 3 4 2 3 4 2 3 4 2 3 4 2 [1, 2, 3] [] [2] [3] [2,3] [] [1] [2] [1, 2] [3] [1, 3] [2, 3] [1, 2, 3] Al invocar una vez más el iterador, cada uno de los ciclos llamará al iterador asociado a la cola de la lista y llegará nuevamente a la de la lista vacía. En ese punto intentará avanzar de la instrucción (1) pero ya no habrá más nada. Por lo tanto el iterador se devuelve sin valor. El ciclo del invocador, al no recibir nada termina su ejecución. Pero después del ciclo ya no hay más código ejecutable. Por lo tanto también se devuelve sin valor asociado y así sucesivamente 6 hasta llegar al invocador original. El ciclo no recibe valor y por tanto termina la ejecución del programa. Nótese que el iterador estudiado devuelve todas las sublistas posibles de una lista dada (similar a calcular el conjunto de las partes de otro conjunto). 3. Ejericios sugeridos 1. De una implementación en pseudocódigo para un iterador que recibe un entero y produzca todos los valores desde cero (inclusive) hasta el número pasado como parámetro (exclusive). Dicho iterador sería equivalente al iterador times de Ruby. 2. Casi todos los segundos exámenes de la materia, como se ha dado durante los últimos años por el prof. Ernesto HernandezNovich y mi persona, tienen una o más preguntas sobre ejecución de programas con iteradores. Les recomiendo buscen esos parciales e intenten resolverlos usando el camino del bien. Pueden escribirme si tienen cualquier duda al respecto. 3. El segundo parcial de EneroMarzo 2012 tiene un iterador llamado misterio. Además de resolver el ejercicio, piensen en cómo funciona y qué relación tiene con la siguiente línea en Haskell, que tiene un comportamiento similar: f = 1 : scanl (+) 1 f Ricardo Monascal / Mayo 2014 7