El tipo compuesto Racional tipo Racional { observador numerR(r : Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r) 6= 0; invariante mcd(numerR(r), denomR(r)) == 1; Algoritmos y Estructuras de Datos I Primer cuatrimestre de 2015 problema crear(n, d: Int) = rac: Racional { requiere d 6= 0; asegura numerR(rac) * d == denomR(rac) * n; } Departamento de Computación - FCEyN - UBA Tipos abstractos de datos - clase 9 problema denom(r: Racional) = d: int { asegura d == denomR(r) ; } Invariante de representación y función de abstracción problema numer(r: Racional) = n: int { asegura d == numerR(r) ; } } 1 TADs Racionales en C++ (Intento 1) 2 ¿Es correcta esta implementación de Racional? class Racional { public: Racional (int,int); int numer(); int denom(); private: int num, den; }; class Racional { public: Racional (int,int); int numer (); int denom (); private: int num, den; }; Racional::Racional (int n, int d) { num = n; den = d; }; Racional::Racional (int n, int d) { num = n; den = d; }; int Racional::numer(){return num;}; int Racional::numer (){return num;}; int Racional::denom(){return den;}; I En el constructor this denota la instancia que se crea. int Racional::denom (){return den;}; I El resto de las funciones tienen un parámetro implı́cito de tipo Racional que se llama this. En general asumimos que es el primero del problema. ¿Qué sucede con Racional (1,0)? y con Racional (2,20).numer()? 3 4 Correctitud de implementación de TADs Invariante de representación para Racional Utilizamos dos predicados I Invariante de representación (InvRep): I Restringe los posibles valores que pueden tomar las variables que definen la representación del TAD. I I I class Racional { public: Racional (int,int); int numer (); int denom (); private: int num, den; // invRep(x): x.den 6= 0 ∧ mcd(x.num, x.den) == 1 }; Por ejemplo, el denominador de un racional no puede ser 0 Debe ser preservado por todas las operaciones que provee el TAD (es decir, debe valer al final de la ejecución de cada operación) Predicado de abstracción (abs): I I I Relaciona la especificación del TAD con su implementación. Mapea instancias de una clase en valores del tipo. Conecta representación interna con observadores. Asume que InvRep es verdadero (también que el invariante del tipo compuesto es verdadero, pero esto es siempre cierto). I Notar que el InvRep se especifica usando los campos de la clase (variables de instancia): num y den. I Asumimos que el tipo de cada campo es una implementación correcta de un tipo del lenguaje de especificación (en este caso, int implementa Int). 5 Corrección de implementación de TADS 6 Preservación de invRep en constructor de Racional Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema crearR(n, d: Int) = rac: Racional { requiere d 6= 0; asegura numerR(rac) * d == denomR(rac) * n; } } Para demostrar que una clase C es una implementación correcta de un tipo compuesto tenemos que mostrar: 1. Todas las operaciones públicas del TAD preservan invRep. Asumimos que ya está definida la operación gcd para int. 2. Los métodos públicos cumplen con su contrato. Racional::Racional(int n, int d){ // vale d 6= 0 (por el requiere) int mcd = gcd(n,d); num = n/mcd; den = d/mcd; // vale num == n/mcd(n, d) ∧ den == d/mcd(n, d) // implica den 6= 0 porque d 6= 0 // implica mcd(num, den) == mcd(n, d)/mcd(n, d) (porque mcd(a/c, b/c) == mcd(a, b)/c cuando c es un divisor común) // implica mcd(num, den) == mcd(n, d)/mcd(n, d) == 1 // implica invRep(this) } 7 8 Preservación de invRep en constructor de Racional Preservación de invRep en funciones sin modifica Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema copiar(a: Racional) = rac: Racional { asegura numerR(rac) == numerR(a); asegura denomR(rac) == denomR(a); } } problema denom(r: Racional) = d: int { asegura d == denomR(r) ; } Racional::Racional(Racional a){ // vale invRep(a) Asumimos que a es correcto NO vale invRep(this) porque estamos construyendo this num = a.num; den = a.den; // vale num == a.num ∧ den == a.den // implica den 6= 0 porque a.den 6= 0 por invRep(a) // implica mcd(num, den) == 1(porque mcd(a.num, a.den) == 1 por invRep(a) // implica invRep(this) // notar que vale invRep(a) porque a no se modifica } int Racional::denom(){ // vale invRep(pre(this)) return den; // implica invRep(this) } this existe y asumimos que es correcto (porque this == pre(this)) Este caso (al igual que numer ) es trivial por el código de denom. En general vamos a tener que probar que invRep vale al final (como en los casos anteriores). 10 9 Preservación de invRep en funciones que modifica this Preservación de invRep en función que modifica parámetro Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema cuadNum(a: Racional) { modifica a; asegura numerR(a) == numerR(pre(a))*numerR(pre(a)); asegura denomR(a) == denomR(pre(a)); } } void Racional::cuadNum(){ // vale invRep(pre(this)) Asumimos que this es correcto num = num*num; // vale num == pre(this).num ∗ pre(this).num ∧ den == pre(this).den // implica den 6= 0 (porque pre(this).den 6= 0 por invRep(pre(this))) // implica mcd(num, den) == mcd(pre(this).num ∗ pre(this).num, pre(this).den) // implica mcd(num, den) == 1 (usamos la propiedad mcd(a ∗ b, c) divide a mcd(a, c) ∗ mcd(b, c)) (en particular: mcd(a ∗ a, c) divide a mcd(a, c) ∗ mcd(a, c)) (y dado que mcd(pre(this).num, pre(this).den) == 1 por invRep(pre(this))) (mcd(pre(this).num ∗ pre(this).num, pre(this).den) == 1 para dividir a 1 ∗ 1) // implica invRep(this) } 11 Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema swapR(a: Racional, b: Racional) { modifica a,b; asegura numerR(a) == numerR(pre(b)) ∧ numerR(b) == numerR(pre(a)); asegura denomR(a) == denomR(pre(b)) ∧ denomR(b) == denomR(pre(a)) } } void Racional::swapR(Racional& b){ // vale invRep(pre(this)) ∧ invRep(pre(b))Asumimos que this y b son correctos swap(num, b.num); swap(den, b.den); // vale num == pre(b).num ∧ den == pre(b).den∧ b.num == pre(this).num ∧ b.den == pre(this).den // implica den 6= 0 (porque pre(b).den 6= 0 por invRep(pre(b))) // implica mcd(num, den) == mcd(pre(b).num, pre(b).den) == 1 (porque vale invRep(pre(b))) // implica invRep(this) // implica b.den 6= 0 (porque pre(this).den 6= 0 por invRep(pre(this))) // implica mcd(b.num, b.den) == mcd(pre(this).num, pre(this).den) == 1 (porque vale invRep(pre(this))) // implica invRep(b) es necesario probarlo porque b se modifica } 12 Corrección de implementación de TADS Satisfacción de contratos Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0 invariante mcd(numerR(r ), denomR(r )) == 1 problema crearR(n, d: Int) = rac: Racional { requiere d 6= 0; asegura numerR(rac) * d == denomR(rac) * n; } } Para demostrar que una clase C es una implementación correcta de un tipo compuesto tenemos que mostrar: 1. Todas las operaciones públicas del TAD preservan invRep. Racional::Racional(int n, int d){ // vale d 6= 0 (por el requiere) int mcd = gcd(n,d); num = n/mcd; den = d/mcd; // vale num == n/mcd(n, d) ∧ den == d/mcd(n, d) // implica invRep(this) : den 6= 0 ∧ mcd(num, den) == 1 // implica num/den == n/d (porque den 6= 0, d 6= 0, mcd(n, d) 6= 0) // implica num ∗ d == den ∗ n } 2. Los métodos públicos cumplen con su contrato. Notar que num ∗ d == den ∗ n NO es el la poscondicion de crearR Necesitamos relacionar la implementación con los observadores del tipo compuesto 13 Predicado de abstracción I 14 Predicado de abstracción de Racional Predicado que relaciona una instancia concreta de una clase (implementación) con un elemento de su tipo (especificación). class Racional { public: Racional (int,int); int numer (); int denom (); private: int num, den; // invRep(x): x.den 6= 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den }; abs(imp : clase, esp : TipoCompuesto) I Se asume que abs(imp : clase, esp : TipoCompuesto) vale solo para las instancias de la implementación que satisfacen el invariante de representación. I Formalmente invRep(imp) ⇒ abs(imp : clase, esp : TipoCompuesto) invRep y abs están estrechamente ligados (vamos a volver) Importante: Sólo se puede utilizar el predicado abs(imp, a) en estados donde vale invRep(imp) Convención: A menudo escribiremos abs(a, ae ) para denotar con ae el valor del tipo (especificación) 15 16 Ahora sı́: Satisfacción de contratos Satisfación de contrato en constructor con párametro Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema crearR(n, d: Int) = rac: Racional { requiere d 6= 0; asegura numerR(rac) * d == denomR(rac) * n; } } problema copiar(a: Racional) = rac: Racional { asegura numerR(rac) == numerR(a); asegura denomR(rac) == denomR(a); } } // invRep(x): x.den 6= 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den // invRep(x): x.den 6= 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den Racional::Racional(int n, int d){ // vale d 6= 0 (por el requiere) int mcd = gcd(n,d); num = n/mcd; den = d/mcd; // implica invRep(this) : den 6= 0 ∧ mcd(num, den) == 1 // implica abs(this, rac) (porque vale invRep(this) y this es el resultado) // implica num ∗ d == den ∗ n // implica numerR(rac) ∗ d == denomR(rac) ∗ n (por abs(this,rac)) } 17 Satisfacción de contrato en función sin modifica Racional::Racional(Racional a){ // vale invRep(a) ∧ abs(a, ae ) Asumimos que a es correcto usamos ae para hablar de a de la especificación // implica numerR(ae ) == a.num ∧ denomR(ae ) == a.den num = a.num; den = a.den; // vale num == a.num ∧ den == a.den // implica num == numerR(ae ) ∧ den == denomR(ae ) (por abs(a, ae ))) // implica invRep(this) (lo probamos antes) // implica abs(this, rac) (porque vale invRep(this) y this es el resultado) // implica numerR(rac) == numerR(ae ) ∧ denomR(rac) == denomR(ae ) (porque vale abs(this, rac)) } 18 Satisfacción de contratos con modifica this y parámetro Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema swapR(a: Racional, b: Racional) { modifica a,b; asegura numerR(a) == numerR(pre(b)) ∧ numerR(b) == numerR(pre(a)); asegura denomR(a) == denomR(pre(b)) ∧ denomR(b) == denomR(pre(a)) } } Tipo Racional{ observador numerR(r: Racional): Int; observador denomR(r: Racional): Int; invariante denomR(r ) 6= 0; invariante mcd(numerR(r ), denomR(r )) == 1; problema denom(r: Racional) = d: int { asegura d == denomR(r) ; } // invRep(x): x.den 6= 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den int Racional::denom (){ // vale invRep(pre(this)) ∧ abs(pre(this), r ) return den; // vale d == den (porque den es el resultado) // implica invRep(this) (porque this == pre(this)) // implica abs(this, r ) (porque this == pre(this) y abs(pre(this), r )) // implica d == denomR(r ) (porque vale abs(this, r )) } 19 // invRep(x): x.den > 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den void Racional::swapR(Racional& b){ // vale invRep(pre(this)) ∧ invRep(pre(b)) ∧ abs(pre(this), pre(a)) ∧ abs(pre(b), pre(be )) swap(num, b.num); swap(den, b.den); // vale num == pre(b).num ∧ den == pre(b).den ∧ b.num == pre(this).num ∧ b.den == pre(this).den // implica invRep(this) ∧ invRep(b) (salteando algunos pasos) // implica abs(this, a) ∧ abs(b, be ) (porque valen los invariantes) // implica numerR(a) == numer (pre(be )) ∧ denomR(a) == denomR(pre(be )) ∧ numerR(be ) == numerR(pre(a)) ∧ denomR(be ) == denomR(pre(a)) (usando las abs) } 20 Contratos con requiere Relación entre invRep y abs invRep y abs no son únicas: En general hay varias elecciones. problema inversa(a: Racional) { modifica a; requiere numerR(pre(a)) 6= 0; asegura numerR(a) ∗ numerR(pre(a)) == denomR(a) ∗ denomR(pre(a)); } Ejemplos: // invRep(x): x.den > 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den // invRep(x): x.den > 0 ∧ mcd(x.num, x.den) == 1 // abs(x,rac): numerR(rac) == x.num ∧ denomR(rac) == x.den void Racional::inversa(){ // vale invRep(pre(this)) ∧ abs(pre(this), pre(a)) // vale numerR(pre(a)) 6= 0 (por requiere) // implica pre(this).num 6= 0 (por abs(pre(this),pre(a))) int aux = num; num = den; den = aux; // implica num == pre(this).den ∧ den == pre(this).num (por transformación de estados) // implica den 6= 0 (porque pre(this).num 6= 0) // implica mcd(num, den) == 1(porque mcd(pre(this).num, pre(this).den) == 1) // implica invRep(this) ∧ abs(this, a) // implica num ∗ pre(this).num == den ∗ pre(this).den // implica numerR(a) ∗ numerR(pre(a)) == denomR(a) ∗ denomR(pre(a)) (porque vale abs(this, a) y abs(pre(this), pre(a))) } // invRep(x): x.den 6= 0 // abs(x,rac): numerR(rac) == x.num/mcd(x.num, x.den) ∧ denomR(rac) == x.den/mcd(x.num, x.den) // invRep(x): true // abs(x,rac): (x.den 6= 0 ∧ numerR(rac) == x.num/mcd(x.num, x.den) ∧ denomR(rac) == x.den/mcd(x.num, x.den)) ∨ (x.den == 0 ∧ numerR(rac) == 1 ∧ denomR(rac) == 1) I Notar que para invReps más débiles se requiere un abs más complicado. I La elección impacta en la implementación de las operaciones Ejercicio: implementar y probar la correctitud de Racional con las alternativas anteriores 22 21 Relación entre invRep y abs Funciones auxiliares (Helpers) class Racional { public: ... void suma(Racional); private: int num, den; // invRep(this): this.den > 0 ∧ mcd(this.num, this.den) == 1 void reconstruir() }; void Racional:: suma(Racional r){ num = num*r.den + r.num * den; den =den*r.den; reconstuir(); } La elección no es libre. Por ejemplo, no vale la siguiente combinación. // invRep(x): true // abs(x,rac): numerR(rac) == x.num/mcd(x.num, x.den) ∧ denomR(rac) == x.den/mcd(x.num, x.den) Tomemos una instancia donde x.num == 1 y x.den == 0. Notar que mcd(1, 0) == 1. Luego, por abs(x, rac) vale denomR(rac) == x.den/1 == 0 // helper: void Racional::reconstruir() { int mcd = gcd(num, den); num = num/mcd; den = den/mcd; } Esto viola el invariante de tipo, que requiere denomR(r ) 6= 0 para todo r. Luego, invRep y abs deben ser definidos de forma tal que invRep(x) ∧ abs(x, rac) no contradigan el invariante de tipo sobre rac. I El invariante de representación no necesariamente vale a la entrada y salida de una función auxiliar. I las funciones auxiliares deben ser privadas 23 24 Prueba con auxiliares Con las auxiliares vamos a trabajar como con cualquier bloque de código, dando su precondicion y su poscondición. void Racional::reconstruir() { // PAux : True int mcd = gcd(num, den); num = num/mcd; den = den/mcd; // implica QAux : mcd(num, den) == 1 } por transformación de estados void Racional:: suma(Racional r){ num = num*r.den + r.num * den; den =den*r.den; // implica PAux : True reconstuir(); // vale QAux } 25