Desarrollo Formal de Programas. Notas de clase (1)

Anuncio
Desarrollo Formal de Programas. Notas de clase (1)
Camilo Rueda
[email protected]
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
Descargar