Programación imperativa Entidad fundamental: variables Algoritmos y Estructuras de Datos I I Corresponden a posiciones de memoria (RAM). I Cambian explı́citamente de valor a lo largo de la ejecución de un programa. Segundo cuatrimestre de 2014 Tres instrucciones: Departamento de Computación - FCEyN - UBA I asignación (y llamadas a funciones), Programación imperativa - clase 6 I condicional, Ciclos y arreglos I ciclo Un programa es una lista de instrucciones, que se ejecutan una tras otra. La semántica de un programa se define mediante la composición de la semántica de las instrucciones por transformación de estados. 1 Programación imperativa 2 Ciclos Aunque muchos lenguajes imperativos permiten implementar funciones recursivas, el mecanismo fundamental de cómputo no es la recursión. while (B) {cuerpo del ciclo}; Es Es Es Es Es Es Es Es Es Es Es Es Es Es la la la la la la la la la la la la la la iteración iteración iteración iteración iteración iteración iteración iteración iteración iteración iteración iteración iteración iteración (o (o (o (o (o (o (o (o (o (o (o (o (o (o repetición repetición repetición repetición repetición repetición repetición repetición repetición repetición repetición repetición repetición repetición o o o o o o o o o o o o o o ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). ciclos). B: expresión booleana del lenguaje de programación. Se la llama guarda. {cuerpo de ciclo}: es un bloque de instrucciones entre llaves. Se repite mientras valga la guarda B, cero o más veces. La ejecución del ciclo termina si y solo si el ciclo se repite una cantidad finita de veces. Esto ocurre si y solo si la guarda B llega a ser falsa. Si el ciclo termina, el estado resultante es el estado posterior a la última instrucción del cuerpo del ciclo. 3 4 Ejemplo de un ciclo Correctitud de un ciclo respecto de una especificación problema fact(x : Int) = r : Int{ requiere x ≥ 0 Q asegura r == [1..x] } Introducimos en el lenguaje de especificación PC : precondición del ciclo QC : postcondición del ciclo B: guarda donde B corresponde a la guarda B del lenguaje de programación. En funcional: fact :: Int -> Int fact 0 = 1 fact i | i>0 = i * (fact (i-1)) int g(int x) { .... // estado antesdelciclo // vale PC while (B) { cuerpo del ciclo (una o más instrucciones); } // estado despuesdelciclo // vale QC .... } En imperativo: int fact (int x) { int f = 1; int i = 1; while (i ≤ x) { f = f * i; i = i + 1; } return f; } 5 Correctitud de un ciclo PC : QC : B: I: 6 Invariante precondición del ciclo, postcondición del ciclo, guarda invariante int g (int x) { .... // estado antesdelciclo // vale PC // implica I while (B) { // estado antes // vale B ∧ I cuerpo del ciclo (una o más instrucciones) // estado despues // implica I } // vale I ∧¬B; implica QC .... } Expresión booleana del lenguaje de especificación que se mantiene verdadera en los estados antes, durante y después de la ejecución de un ciclo. 7 I es análogo a la hipótesis inductiva que usamos para demostrar la correctitud de un programa recursivo en programación funcional. I conviene darlo al escribir el ciclo porque expresa la idea del ciclo. I no hay forma algorı́tmica de encontrarlo 8 Un ejemplo Ejemplo sumat(int x) int sumat (int x) { int s = 0, i = x; while (i > 0) { // estado a s = s + i; i = i - 1; // estado b } return s; } problema sumat(x : Int) = r : Int{ requiere x ≥ 0 P asegura r == [0..x] } En funcional: sumat :: Int -> Int sumat 0 = 0 sumat i = i + (sumat (i-1)) En imperativo: int sumat (int x) { int s = 0, i =x; while (i ≥ 0) { s = s + i; i = i - 1; } return s; } Estados para x == 4 i@a s@b i@b 4 4 3 3 7 2 2 9 1 1 10 0 P i@a ([email protected]] i@b P En estado a y b se cumple 0 ≤ i ≤ x y s == (i..x] (pero no en el medio) Cuando i == 0, la ejecución del ciclo termina. s@a 0 4 7 9 P ([email protected]] 9 Ejemplo sumat(int x) 10 Transformación de estados dentro del ciclo La x no cambia porque es de entrada y no aparece en modifica ni en local. //estado e1 //vale B: i >0 P //vale I : 0 ≤ i ≤ x ∧ s == (i..x] s = s + i; //estado e2 //vale i = i@e1 ∧ s == s@e1 + i@e1 i = i - 1; //estado e3 //vale i == i@e2 − 1 ∧ s == s@e2 //implica i == i@e1 − 1 ∧ s == s@e1 + i@e1 porque i@e2 == i@e1 y s@e2 == s@e1 + i@e1. //implica 0 ≤ i ≤ x porque 0 < i@e1 ≤ x y i@e3 == i@e1 − 1 P //implica s == (i..x] P porque s@e1 == ([email protected]] y int sumat (int x) { int s = 0; i = x; //vale PC : s == 0 ∧ i == x while (i > 0) { P //invariante I : 0 ≤ i ≤ x ∧ s == (i..x] //estado e1 s = s + i; //estado e2 i = i - 1; //estado e3 } P //vale QC : i == 0 ∧ s == (0..x] return s; P //vale result == [0..x] } s@e3 == s@e2 == s@e1 + i@e1 == P ([email protected]] 11 P P ([email protected]] + i@e1 == (i@e1 − 1..x] == 12 ¿Cómo demostramos que el ciclo termina? Terminación y correctitud de un ciclo respecto de una especificación Acotamos superiormente la cantidad de iteraciones restantes del ciclo mediante una expresión variante (v ) //vale PC ; while (B) { //invariante I ; cuerpo } //vale QC ; Es una expresión del lenguaje de especificación, de tipo Int I definida a partir de las variables del programa I debe decrecer estrictamente en cada iteración //estado e; //vale B ∧ I ; cuerpo //vale v < v @e; Un ciclo termina si después de una cantidad finita de iteraciones se cumple la negación de la guarda. Un ciclo es correcto respecto a la especificación si comenzando su ejecución en un estado que cumple PC el ciclo termina en un estado que cumple QC . Damos una cota (c) (valor entero fijo, por ejemplo 0 o −8) tal que si es alcanzado por la expresión variante, está garantizado que la ejecución alcanza un estado donde vale la negación de la guarda, y por lo tanto sale del ciclo. 13 Ejemplo de expresión variante 14 Ejemplo de demostración de expresión variante decreciente. Recordemos el programa sumat y la transformación de estados P //invariante I : 0 ≤ i ≤ x ∧ s == (i..x] //variante v : i //estado e1 //vale B ∧ I P //implica 0 < i ≤ x ∧ s == (i0..x] s = s + i; //estado e2 //vale i == @e1 ∧ s == s@e1 + e@1 i = i - 1; //estado e3 //vale i == i@e2 − 1 ∧ s == s@e2 int sumat (int x) { int s = 0; i = x; //vale PC : s == 0 ∧ i == x while (i > 0) { P //invariante I : 0 ≤ i ≤ x ∧ s == (i..x] s = s + i; i = i - 1; } P //vale QC : i == 0 ∧ s == (0..x] return s; P //vale result == [0..x] } Debemos ver que la expresión variante v : i es decreciente. Es decir, debemos ver que v @e1 > v @e3. Pero esto es inmediato, porque v @e1 == i@e1 y v @e3 == i@3 == i@e2 − 1 == i@e1 − 1. La expresión variante es un indicador de la distancia a la terminación del ciclo. //variante v : i, cota 0 15 16 Teorema de terminación Observaciones sobre terminación Sea el siguiente programa: Sea I el invariante de un ciclo con guarda B, v una expresión variante v (expresión entera decreciente) y c una cota tal que (I ∧ v ≤ c) → ¬B. Entonces el ciclo termina. int dec1(int x) { while (x > 0) x = x − 1; return x; } Demostración. Sea vj el valor que toma v en el estado que resulta de ejecutar el cuerpo del ciclo por j-ésima vez. Dado que v es de tipo Int, para todo j, vj ∈ Int. Supongamos Como v es estrictamente decreciente, v1 > v2 > v3 > . . . , existe un k tal que vk ≤ c. Dado que (I ∧ v ≤ c) → ¬B, en el estado alcanzado luego de k iteraciones vale ¬B. Por lo tanto el ciclo termina. Pc : x ≥ 0. I :x ≥0 v = x es estrictamente decreciente, cota c = 0. Dado que B : x > 0, se cumple v ≤ c → ¬B y por simple cálculo proposicional, (I ∧ v ≤ c) → ¬B. Es decir, no fue necesario usar el invariante I . ¿Qué pasarı́a si v fuese de tipo Float? ¿Funcionarı́a esta demostración? 17 Observaciones sobre terminación 18 Expresión variante y cota int dec2(int x) { while (x != 0) x = x − 1; return x; } Sea v una función variante y sea c su cota asociada. I v 0 = v − c es una función variante con cota asociada 0. Sin pérdida de generalidad, podemos suponer siempre una función variante y cota asociada 0. Supongamos Pc : x ≥ 0 I :x ≥0 v = x, cota c = 0. Dado que B : not(x == 0), (I ∧ v ≤ c) → ¬B I v es estrictamente decreciente en las sucesivas iteraciones del ciclo. En cada iteración decrece en 1 o más unidades. Por lo tanto el valor de la expresión variante no necesariamente mide la cantidad de iteraciones (restantes) de la ejecución del ciclo. Notemos que en este caso sı́ necesitamos usar I ya que x ≤ 0 6→ x == 0 pero (x ≥ 0 ∧ x ≤ 0) → x == 0. 19 20 Teorema de correctitud de un ciclo Teorema del Invariante Sea while (B) { cuerpo } un ciclo con guarda B, precondición PC y poscondición QC . Sea I un predicado booleano, v una expresión variante, c una cota, y sean los estados e1 y e2 ası́: while (B) { //estado e1 cuerpo //estado e2} Si valen: Sea PC , QC , I , B, v la especificación de un ciclo que termina. Si se cumplen las relaciones de fuerza PC → I y (I ∧ ¬B) → QC entonces el ciclo es correcto con respecto a su especificación. Demostración. Debemos ver que para variables que satisfagan PC , el estado alcanzado cuando el ciclo termina satisface QC . //estado e1 //vale PC ; while (B) { //vale I ∧ B; cuerpo //vale I ; } //estado e2 //vale QC ; Supongamos que las variables en el estado e1 satisfacen PC como PC → I , entonces en el estado e1 se cumple I . Supongamos que el ciclo ejecuta 0 o más veces. Por definición de invariante en cada iteración, el invariante se restablece. Por hipótesis, el ciclo termina y en el estado e2 vale ¬B. Además, en el estado e2 vale I . Como (I ∧ ¬B) → QC entonces en el estado e2 vale QC . 1. PC → I 2. (I ∧ ¬B) → QC 3. el invariante se preserva en la ejecución del cuerpo, i.e. si I ∧ B vale en el estado e1 entonces I vale en el estado e2 4. v es decreciente, i.e. v @e1 > v @e2 5. (I ∧ v ≤ c) → ¬B entonces para cualquier valor de las variables del programa que haga verdadera PC , el ciclo termina y hace verdadera QC , es decir, el ciclo es correcto para su especificación (PC , QC ). Demostración. Inmediata del Teorema de Terminación (p. 17) y el Teorema de Correctitud (p. 21). 22 21 ¿Cuales de los 5 puntos entran en el primer parcial? Retomamos ejemplo sumat Teoema del Invariante. Sea while (B) { cuerpo } un ciclo con guarda B, precondición PC y poscondición QC . Sea I un predicado booleano, v una expresión variante, c una cota. Si valen: 1. PC → I problema sumat(x : Int) = r : Int{ requiere P : x ≥ P0 asegura r == [0..x] } 2. (I ∧ ¬B) → QC 1. PC → I 3. el invariante se preserva en la ejecución del cuerpo, i.e. si I ∧ B vale en el estado e1 entonces I vale en el estado e2 //vale PC : s == 0 ∧ i == x P //invariante I : 0 ≤ i ≤ x ∧ s == (i..x] 4. v es decreciente, i.e. v @e1 > v @e2 Supongamos que vale PC y veamos que vale I 5. (I ∧ v ≤ c) → ¬B 1. i == x implica i ≥ x. entonces para cualquier valor de las variables del programa que haga verdadera PC , el ciclo termina y hace verdadera QC , es decir, el ciclo es correcto para su especificación (PC , QC ). 2. x ≥ 0 implica i ≥ 0. P P (i..x] == (x..x] == 0 == s. P Por lo tanto, (i == x ∧ s == 0) → (0 ≤ i ≤ x ∧ s == (i..x]). 3. s == 0 y i == x implica Los puntos 1,2 y 5 se demuestran usando lógica proposicional exclusivamente. Los puntos 3 y 4 quedan para después del primer parcial. 23 24 Retomamos ejemplo sumat Retomamos ejemplo sumat 2. (I ∧ ¬B) → QC 5. (I ∧ v ≤ c) → ¬B P // invariante I : 0 ≤ i ≤ x ∧ s == (i..x] // ¬B : ¬(i > 0) P //vale QC : i == 0 ∧ s == (0..x] //invariante I : 0 ≤ i ≤ x ∧ s == //v ≤ c : i ≤ 0 // ¬(B) : ¬(i > 0) Supongamos que vale I ∧ ¬B y veamos que vale cada parte de QC . Por I sabemos que 0 P ≤ i. Por ¬B sabemos i ≤ 0. Entonces, i == 0. Por I sabemos s == (i..x], y recién mostramos que i == 0, luego P s == (0..x]. P (i..x] Supongamos que vale I ∧ (v ≤ c). Debemos ver que vale ¬B. Esto es inmediato ya que (v ≤ c) == i ≤ 0 == ¬B. 26 25 Arreglos Arreglos versus listas I Secuencias de una cantidad fija de variables del mismo tipo. Se declaran con un nombre, un tamaño y un tipo. I Ejemplo: I En el lenguaje de especificación tratamos a ambos con listas por comprensión. int a[10]; // arreglo de 10 elementos enteros int a[]={0,1,2,3,4,5,6,7,8,9}; //Inicializado 2 I -1 1 0 0 2 -1 1 0 0 Nos referimos a los elementos a través del nombre del arreglo y un ı́ndice, entre corchetes, que va de 0 a la dimensión menos uno: Una referencia a una posición fuera del rango (usualmente) da error (en tiempo de ejecución). I El tamaño y el tipo de un arreglo se mantienen invariantes a lo largo de la ejecución. I Longitud Los arreglos tienen longitud fija; las listas, no. I Acceso I I a[0], a[1],..., a[9] //acceso aleatorio I Ambos son secuencias de elementos de un tipo dado. I 27 Los elementos de un arreglo se acceden de manera directa, e independiente de los demás. a[i] accede al elemento en posición i-ésima del arreglo. Tiene dirección de memoria propia, y por lo tanto se le puede asignar un valor. Los elementos de una lista se acceden secuencialmente, empezando por la cabeza. Para acceder al i-ésimo elemento de una lista, hay que obtener i veces la cola y luego la cabeza. 28 Arreglos en C++ I Leyendo un arreglo problema sumarray(a : [Int], tam : Int) = res : Int{ requiere P: tam ==P |a|; asegura Q: res == a[0..|a|); } Los arreglos en C++ son referencias. I No hay forma de averiguar su tamaño una vez que fueron creados; el programa tiene que encargarse de almacenarlo de alguna forma. I Al ser pasados como argumentos, en la declaración de un parámetro no se indica su tamaño. int sumarray(int a[], int tam){ int j = 0; int s = 0; P // Pc: j == 0 ∧ s == a[0..j) while (j <tam) { P // invariante 0 ≤ j ≤ tam ∧ s == a[0..j) // variante tam − j; s = s + a[j]; j++; I Cuando se usan como argumentos de funciones en C y C++, pasan automáticamente como referencia (¡no por copia!). } P // Qc : s == a[0..|a|) return s; P // vale Q: res == a[0..|a|); } 29 Inicialización de arreglos 30 Modificando un arreglo problema init (a : [Int], x : Int, n : Int){ requiere n == |a| ∧ n > 0; modifica a; asegura todos([a[j] == x, j ∈ [0..n)]); asegura todos([a[j] == x, j ∈ [0..n)]); } problema ceroPorUno(a : [Int], tam : Int){ requiere tam == |a|; modifica a; asegura a == [if i == 0 then 1 else i | i ← pre(a)[0..tam)]; } void ceroPorUno(int a[], int tam) { int j = 0; // vale Pc : j == 0 while (j < tam) { // invariante a[0..j) == [if i == 0 then 1 else i | i ← pre(a)[0..j)]; // ∧0 ≤ j ≤ tam ∧ a[j..tam) == pre(a)[j..tam) // variante tam − j; if (a[j] == 0) a[j] = 1; j++; } } void init (int a[], int x, int n) { int i = 0; // vale Pc : i == 0 while (i < n) { a[i]=x; i = i + 1; // invariante I: 0 ≤ i ≤ n ∧todos([a[j] == x, j ∈ [0..i)]) // variantev : n − i } // vale Qc : i == n ∧ todos([a[j] == x, j ∈ [0..i)]) } 31 32