Desarrollo Formal de Programas. Notas de clase (1) Camilo Rueda crueda@atlas.ujavcali.edu.co 19 de septiembre de 2005 Resumen Notas del curso, basadas en la exposición de J Abrial de la metodologı́a B. El curso motiva el pensamiento formal sobre todos los aspectos del desarrollo de software, desde el establecimiento de requerimientos, pasando por la especificación y el diseño y finalizando en la implementación. Se parte de una presentación precisa de los formalismos y notaciones del método B como banco de herramientas para pensar adecuadamente sobre un sistema de software. Con ellos se presentan ejemplos de especificación de programas simples, con el ánimo de justificar la relevancia del método observando la elegancia y precisión de especificaciones y código. Se enfatiza enseguida el aspecto de implementación formal analizando en detalle las obligaciones de prueba de diferentes tipos de operaciones concretas usuales en los programas imperativos. Al final de las notas se introduce la noción de refinamiento formal para dar una idea del proceso de desarrollo de un software complejo y motivar en el estudiante la percepción sobre la utilidad del método para construı́r aplicaciones reales. 1. Motivación A pesar del avance de las metodologı́as de desarrollo de software y la existencia de lenguajes de programación (orientados-objeto, por ejemplo) que respaldan una buena organización del código, es claro que buena parte de las aplicaciones, aún aquellas desarrolladas por equipos de amplia experiencia, tiene fallas no triviales de funcionamiento. Por el despliegue de los medios, son conocidos algunos casos de fallas en el software que han ocasionado grandes pérdidas económicas: La falla en el algoritmo de operaciones flotantes del chip Pentium que obligó al reemplazo de un numeroso lote de procesadores instalados, el error en el software de control de posicionamiento del cohete europeo Ariane V que condujo a su destrucción, o la falla en el software de enrutamiento de llamadas telefónicas de AT&T que causó la caı́da del todo el sistema telefónico del este de los Estados Unidos durante varias horas. Al lado de estos casos notables, existen múltiples ejemplos de software que no alcanza a cumplir las expectativas de usuarios o contratantes. En Colombia serı́a fácil apreciar la amplitud del problema de calidad en el software haciendo un simple recuento de las veces que el usuario encuentra a sus preguntas o solicitudes la enigmática respuesta: “el sistema está caı́do”. Baste recordar el caso del software de conteo de votos, hace 1 algunos años, que dejó de funcionar solamente un dı́a: El de las elecciones!. A menor escala, es frecuente encontrar en las entidades empleados que se han construı́do una merecida reputación porque saben exactamente cuál es la secuencia de operaciones que evita el malfuncionamiento repetido de la aplicación. Por qué el software no funciona? Por qué esta situación parece no impedir el crecimiento de su mercado? El software no tiene, a priori, una caracterı́stica que lo obligue necesariamente a ser incorrecto. Sin embargo, un hábil mercadeo ha conducido a los usuarios a aceptar un comportamiento que encontrarı́an totalmente irracional si se tratara de otro domino: Cuando la aplicación no funciona, hay que esperar una nueva versión que quizá opere mejor y por la cual se cobrará un precio adicional! Traduzcamos esta obligación al dominio de los electrodomésticos: “Si el televisor que acaba de comprar no funciona apropiadamente, por favor espere tres meses y compre además el nuevo modelo, que funciona mejor”. Si el software no debe necesariamente tener fallas, sı́ tiene en cambio una caracterı́stica que lo separa de muchos sistemas en otras áreas: Un programa es un sistema discreto extremadamente sensible. Un cambio mı́nimo en un programa (un bit, por ejemplo), puede modificar su comportamiento de manera catastrófica. Esta alta sensibilidad al cambio es lo que hace equivocada la metodologı́a de pruebas controladas, tan útil en otras ramas de la ingenierı́a, como medio para confirmar que el comportamiento de un producto, en un rango normal de operación, satisface sus especificaciones. En el caso de un programa, el rango normal de uso puede tener muchos puntos de “resonancia” que destruyen por completo su funcionamiento. En principio tales puntos pudieran ser ubicados mediante ensayos, pero en la práctica esto es imposible porque involucrarı́a un número astronómico de pruebas. El problema debe estar entonces en el proceso usual de desarrollo de software que se enfrenta como un asunto de ingenierı́a, cuando es en realidad un asunto de lógica matemática. Es decir, la programación tiene más que ver con la noción de prueba que con la noción de ensayo. La coherencia entre especificación, diseño e implementación debe probarse formalmente a lo largo de todo el proceso. Estas notas pretenden mostrar cómo es esto posible. 2. Especificación formal Para la especificación formal de programas utilizamos la llamada notación-B. La idea es tomar un subconjunto simple pero suficientemente expresivo de la lógica de primer orden que permita especificar sistemas de manera precisa. La técnica de especificación debe ser: 1. Incremental y escalable: La definición precisa de un sistema complejo debe poder construı́rse a partir de principios fácilmente ilustrables mediante ejemplos de sistemas sencillos. 2. Reutilizable: La especificación de un sistema debe ser suficientemente flexible para permitir fácilmente su adaptación a la especificación de sistemas similares en contextos diferentes. 3. Implementable: La especificación debe poder refinarse mediante etapas precisas hasta su implantación en un lenguaje de programación real. 2 4. Formal: La especificación debe facilitar la prueba de propiedades del sistema. La especificación define mediante la noción de predicados todas las observaciones posibles del sistema descrito por ella. Las observaciones son objetos del mundo real (simbólico o fı́sico). Estos objetos existen para el sistema en cuanto pertenecen a conjuntos. Los predicados que describen estas observaciones deben por lo tanto hacer referencia a elementos de conjuntos. Por otra parte, nos interesa describir sistemas que definen procesos. La caracterı́stica fundamental de un proceso es el de atravesar una serie de estados. Cada estado es el conjunto de observaciones de las variables pertinentes al sistema. La observación de una variable particular puede entonces cambiar de un estado a otro. La especificaión debe por lo tanto poder describir la sustitución de unas observaciones por otras. El lenguaje de especificación debe cumplir al menos las siguientes propiedades: 1. Para que las propiedades de un sistema puedan demostrarse, el lenguaje debe ser formal. 2. Debe ser suficientemente expresivo como para especificar sin mayores dificultades cualquier sistema de software. 3. Debe ser “cercano” al lenguaje de implementación para que el paso de diseño a realización pueda también ser formalizable. 4. las estructuras de datos usuales en el software deben poder representarse adecuadamente. El cálculo de predicados cumple las dos primeras condiciones. Para las otras dos es necesario hacer algunas extensiones. Por una parte, concebir las instrucciones usuales de un lenguaje de programación como mecanismos de transformación de un predicado en otro. Por otra, construı́r algunos objetos matemáticos de base. Para lo primero, se define el concepto de sustitución. Para lo segundo, todo lo que se requiere es la noción de conjunto. Estas consideraciones dictan la definición sintáctica de un predicado, que se presenta a continuación. 3 SUSTITUCION [x := E]x [x := E]y [x := E]P ∧ Q [x := E]P ⇒ Q [x := E]¬P [x := E]∀x.P [x := E]∀y.P DEFINICION E y, si x no ocurre libre en y [x := E]P ∧ [x := E]Q [x := E]P ⇒ [x := E]Q ¬[x := E]P ∀x.P ∀y.[x := E]P si x no ocurre libre en y Cuadro 1: Sustitución en predicados 2.1. Sintaxis de predicados y expresiones P,Q E,F V,W C,D ::= | | | | | | ::= | | | | ::= | ::= | | | P ∧Q P ⇒Q ¬P ∀ V. P [V := E]P E=E E∈C V [V := E]F E, F selec(C) C Identif icador V, W C ×D P(C) {V | P } GRAN DE Predicados Expresiones Variables Conjuntos Figura 2.1: Sintaxis de los Predicados En la Figura 2.1 la expresión selec(C) indica la escogencia de un elemento cualquiera del conjunto C. El conjunto GRAN DE es un conjunto distinguido (infinito!). Nos interesa particularmente definir de manera precisa las sustituciones de la forma [x := E]P , que se lee “la sustitución de x por E satisface el predicado P ” (formalmente, aunque no lo escribiremos de este modo, [x := E] ⇒ P ), y [x := E]F , que establece la expresión que resulta de sustituı́r ocurrencias libres de x en F por E. La forma E, F define parejas de expresiones. La idea es poder escribir sustituciones múltiples, como en [x, y := x + y, y ∗ x]. Los demás predicados son los estándar en la lógica de predicados. Veremos que las sustituciones permiten modelar cambios de estado y por lo tanto las “instrucciones” en un lenguaje de programación. Este hecho nos permite cruzar la frontera entre especificación e implementación. Obviamente suponemos además que disponemos de las reglas de inferencia y axiomas usuales del 4 SUSTITUCION [x := x]F [x := E]F [y := E][x := y]F [x := G][y := E]F DEFINICION F F si x no ocurre libre en F [x := E]F si y no ocurre libre en F [y := [x := G]E][x := G]F si y no ocurre libre en G Cuadro 2: Sustitución en expresiones cálculo de predicados. Suponemos también las extensiones obvias a la sintaxis para definir sustituciones múltiples: [x, y := G, H]F se define como [z := H][x := G][y := z]F si x no ocurre en y, y además z no ocurre libre en ninguna variable o expresión. En la definición de la sintaxis de los conjuntos la única operación es la de pertenencia (e ∈ C). Otras pueden definirse a partir de ella. Por ejemplo: 1. s ⊆ t =def s ∈ P(t), donde P(t) es el conjunto de subconjuntos (partes) de t. 2. s ⊂ t =def s ⊆ t ∧ s 6= t 3. s ∪ t =def {x | x ∈ s ∨ x ∈ t} Otras operaciones, tales como intersección y diferencia de conjuntos, pueden definirse de manera similar. 2.2. Relaciones binarias Una noción de gran utilidad en las especificaciones para modelar observaciones compuestas es la de relaciones binarias entre dos conjuntos u y v. Escribimos u ↔ v para denotar el conjunto de todas las relaciones binarias entre u y v. Su definición es simplemente: u ↔ v ≡ P(u × v), es decir, todos los conjuntos de parejas posibles construı́das con elementos de u y de v. Una relación binaria particular p es un elemento de u ↔ v. Definimos también el inverso, dominio y rango de una relación binaria en la manera usual: p−1 es {b, a|(b, a) ∈ (v × u) ∧ (a, b) ∈ p} Es decir, la relación inversa no es más que el conjunto de parejas en las que el primero y segundo componente han sido intercambiados. Note que siendo las funciones casos particulares de relaciones, la anterior definición establece el inverso de cualquier función, aún de aquéllas que no son inyectivas. 5 El dominio y el rango de las relaciones son los conjuntos de primeros y segundos componentes de las parejas: dom(p) es {a|a ∈ u ∧ ∃b.(b ∈ v ∧ (a, b) ∈ p} ran(p) es dom(p−1 ) La relación identidad se puede definir ası́: id(u) = {a, b | a, b ∈ u × u ∧ a = b} Composición de relaciones binarias es también útil: p; q se define como {a, c|(a, c) ∈ u × w ∧ ∃b.((a, b) ∈ p ∧ (b, c) ∈ q} Vamos a utilizar frecuentemente la composición de relaciones binarias en las especificaciones. La anterior definición aplica también, obviamente, para el caso de funciones. También las parejas de la relación cuyo dominio (o rango) pertenece a un subconjunto dado del dominio o rango: s / p es {a, b|a ∈ s ∧ (a, b) ∈ p}, o también s / p =def id(s); p p . s es {a, b|b ∈ s ∧ (a, b) ∈ p}, o también p . s =def p; id(s) s̄ / p es {a, b|a 6∈ s ∧ (a, b) ∈ p}, o también (dom(p) − s) / p p . s̄ es {a, b|b 6∈ s ∧ (a, b) ∈ p} p[w] = ran(w / p) Especialmente útil para modelar “cambios” en estructuras de datos es la operación: q <← p = (dom(p) / q) ∪ p EJEMPLO p = {3 7→ 5, 3 7→ 9, 6 7→ 3, 9 7→ 2} q = {2 7→ 7, 3 7→ 4, 5 7→ 1, 9 7→ 5} 6 p−1 = {5 7→ 3, 9 7→ 3, 3 7→ 6, 2 7→ 9} dom(p) = {3, 6, 9} ran(p) = {2, 3, 5, 9} p; q = {3 7→ 1, 3 7→ 5, 6 7→ 4, 9 7→ 7} s = {4, 7, 3} t = {4, 8, 1} s / p = {3 7→ 5, 3 7→ 9} p . t = ∅} s̄ / p = {6 7→ 3, 9 7→ 2} p . t̄ = {3 7→ 5, 3 7→ 9, 6 7→ 3, 9 7→ 2} q <← p = {2 7→ 7, 3 7→ 5, 3 7→ 9, 5 7→ 1, 6 7→ 3, 9 7→ 2} Nos interesa notar también funciones parciales y totales: s 6→ t es {r|r ∈ s ↔ t ∧ ∀(x, y, z).(x, y, z) ∈ x × y × z ∧ (x, y) ∈ r ∧ (x, z) ∈ r ⇒ y = z} Es decir, una función parcial es una relación en la que no hay dos parejas distintas con el mismo primer elemento. La función total s → t es una función parcial cuyo dominio es exactamente s. Por ejemplo, la función diva ∈ N 6→ N , definida como: diva (x) = a/x es una función parcial (indefinida para x = 0). El siguiente ejemplo es una especificación de una base de datos simple. EJEMPLO hombres ⊆ P ERSON A mujeres = P ERSON A − hombres maridos ∈ mujeres 6→ hombres madres ∈ P ERSON A 6→ dom(maridos) esposas = maridos−1 cónyuge = maridos ∪ esposas padre = madre; maridos niños = (madre ∪ padre)−1 hijas = niños . mujeres 7 pariente = (niños−1 ; niños) − id(P ERSON A), donde id(s) = {a, b|(a, b) ∈ s × s ∧ a = b} hermano = pariente . mujer parienteP olítico = (pariente; cónyuge) ∪ (cónyuge; pariente ∪ (cónyuge; pariente; cónyuge) 2.3. observaciones La representación de las entidades observables se construye mediante objetos matemáticos simples: Números naturales, secuencias finitas, árboles, etc. A continuación definimos estos objetos. 1. Naturales. definidos por las propiedades: 0∈N ∀n.(n ∈ N ⇒ sucesor(n) ∈ N Propiedades como la inducción pueden definirse utilizando sustituciones: [n := 0]P ∀n.(n ∈ N ∧ P ⇒ [n := sucesor(n)]P ⇒ ∀n.(n ∈ N ⇒ P ) 2. La función predecesor = sucesor−1 está definida para los naturales sin el cero (o sea, N1 ). 3. secuencias finitas. Son colecciones ordenadas de objetos (no necesariamente distintos). Se definen recursivamente utilizando la operación de inserción (denotada x → s, insertar x en la secuencia s), que se precisa a continuación: x → s = {1 7→ x} ∪ (predecesor; s), suponiendo la condición: x ∈ u ∧ s ∈ N 6→ u La expresión (predecesor; s) denota una función (llamémosla ps) que resulta de la composición de dos funciones. ps es una función de un argumento, que podrı́amos escribir ası́: ps(x) = s(x − 1) La definición de secuencia puede entonces formalizarse ası́ (dondu sec(u) es el conjunto de todas las secuencias construı́das con elementos de s): [] ∈ sec(u) ∀(x, t).((x, t) ∈ u × sec(u) ⇒ (x → t) ∈ sec(u) También puede definirse como: sec(u) = S n∈N (1..n) →u OPERACIONES: 8 Inserción al final: [] ← y es igual a y → [] (x → t) ← y es x → (t ← y) Tamaño: tam([]) = 0 tam(x → t) = 1 + tam(t) reverso: rev([]) = [] rev(x → t) = rev(t) ← x Proyecciones: t ↑ n = (1..n)/t (condición: t es secuencia y n ∈ (0..tam(t))). Obtiene los n primeros elementos de la secuencia. t ↓ n = (sumen ; ((1..n) / t (es decir, la secuencia sin los primeros n elementos). primero(t) = t(1) ult(t) = t(tam(t)) cola(t) = t ↓ 1 f rente(t) = t ↑ (tam(t) − 1) Por abreviación, escribimos las secuencias cuyo dominio son los naturales en orden creciente sin indicar los ı́ndices. Por ejemplo: s = [1 7→ 5, 2 7→ 3, 3 7→ 7] se escribe s = [5, 3, 7] 9