Repetición Determinada e Indeterminada - LDC

Anuncio
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
Descargar