Programas Imperativos
Especificaciones (PI)
Terna de Hoare
Si tenemos dos predicados y , y tenemos una sentencia , podemos preguntarnos si vale o no lo siguiente:
“Para todo estado inicial que cumple , la ejecución de termina en un estado final que satisface ”, o también,
Este predicado puede ser True o False, y se denomina Terna de Hoare.
Por ejemplo:
En este caso, , ya que si , entonces .
El predicado puede ser válido para cualquier estado, o puede ser válido para un conjunto de estados.
El predicado es válido para cualquier estado, al igual que .
es más fuerte que .
es más débil que .
Podemos pensar en un estado débil si exige menos condiciones para cumplirse, y en un estado fuerte si exige más condiciones.
Por ejemplo, si y , entonces es más fuerte que .
Weakest Precondition
Dada la sentencia y el predicado , podemos preguntarnos cuál es el predicado más débil que garantiza que se cumpla después de ejecutar . Este predicado se llama Weakest Precondition y se denota .
La relación entre la Terna de Hoare y la Weakest Precondition es la siguiente:
Derivaciones (PI)
A través de una especificación dada como una terna:
Podemos derivar el programa incógnita con la precondición y la postcondición dadas.
Para ello recurrimos a los predicados asociados a la Terna o a la definición de Weakest Precondition. De manera tal que se cumpla que .
Skip
Si tenemos una especificación de la forma:
Podemos determinar si es un suponiendo y partiendo de la postcondición :
En este caso sencillo, podemos determinar que es un , ya que sin necesidad de ejecutar ninguna sentencia:
Asignación
Supongamos que no es un skip ( no implica ) podemos probar tomando las variables del programa y asignarlas a incógnitas.
Tomemos el siguiente ejemplo:
Por intuición sabemos que puede ser . Sin embargo, debemos determinar el valor de . Podemos probar asignar a una incógnita y usar la definición de Weakest Precondition:
Entonces nuestro programa queda:
Condicional
Si por ejemplo quisieramos saber el máximo entre dos números sin usar el operador , debemos descubrir la sentencia de:
(Tanto como son variables de especificación)
No debemos usar , pero podemos probar asignando a una variable incógnita y aplicar la definición de Weakest Precondition:
Siguiendo el enunciado no podemos usar el operador , podemos resolver esto mediante un análisis por casos:
Caso 1:
Caso 2:
Derivando la asignación, llegamos a un análisis por casos, por lo que podemos usar , donde cada caso es una guarda para determinar el programa final:
Cuando se utiliza el cuantificador de conteo en una especificación, se puede terminar en un análisis por casos si se utiliza el rango unitario:
Por ejemplo:
Tendríamos las dos posibilidades de que la variable sea múltiplo de 6 o no.
Secuenciación
Muchas veces tenemos un problema principal y otro problema accesorio que resolver.
Por ejemplo, la siguiente especificación:
Primero podemos resolver el problema principal (sumar todos los elementos), y dividirlos por (problema accesorio).
Cambiando un poco la estructura y agregando una variable sería:
Una vez realizado el trabajo de derivación en y , daremos con el programa final.
Repetición
Las repeticiones serán necesarias cuando necesitemos ejecutar una misma acción varias veces.
Por ejemplo, si queremos sumar los elementos de un arreglo, necesitaremos recorrerlo.
Para poder determinar la inicialización y el cuerpo de ciclo, necesitamos primero proponer una invariante y la guarda.
El invariante debe cumplir las siguientes condiciones:
-
1) (El invariante debe ser verdadero al inicio)
-
2) (El cuerpo de ciclo mantiene el invariante)
-
3) (El invariante implica la postcondición al terminar el ciclo)
-
4) El ciclo debe terminar
El problema surge en como determinar el invariante. Para ello podemos hacer un reemplazo de constante por variable
Podemos reemplazar la constante por una variable , y proponer un invariante :
Determinamos que debe ser menor o igual a para no exceder el rango del arreglo.
Una guarda puede ser , ya que queremos recorrer todos los elementos del arreglo.
En base a las condiciones anteriores de invariante y guarda, podemos proponer un ciclo:
Donde es la inicialización y es el cuerpo del ciclo. Ahora podemos derivar ambos para llegar al programa final.
Para , podemos probar con , la posición a indexar será por defecto :
Tenemos que . Ahora podemos derivar probando con , queremos aumentar la posición a indexar:
Por lo tanto, . Ahora podemos dar con el programa final:
Ejercicios resueltos - Derivaciones y evaluaciones (PI)
1.
Ejecutar el programa para el arreglo y mostrar los cambios de estado:
Debemos considerar el estado al inicio del programa, en los distintos pasos del ciclo y al finalizar el programa:
2.
Dado un arreglo , derivar un programa que resuelva el problema:
Este problema involucra el uso de un ciclo. Para resolverlo podemos aplicar la técnica de reemplazo de constante por variable. Definimos una variable que recorrerá el arreglo y definimos una invariante :
Nos interesa que sea menor que para no exceeder el rango del arreglo.
Esa será nuestra guarda: .
Con esto podemos seguir la siguiente estructura:
Si queremos recorrer el arreglo, debemos partir de la posición . La inicialización sería:
Por lo tanto, . Ahora podemos derivar probando con , queremos aumentar la posición a indexar:
La expresión no es programable. Pero podríamos definir una nueva variable tal que:
Para hacer su uso debemos declarar esta nueva variable y fortalecer la invariante:
Debemos volver a calcular la inicialización , partiendo de :
Por lo tanto, . Derivamos probando con aumentamos la posición a indexar:
Por lo tanto, . Ahora podemos dar con el programa final:
3.
Dado un arreglo , derivar un programa que resuelva el problema:
Este problema involucra un ciclo, ya que debemos recorrer el arreglo y comparar los elementos.
Podemos aplicar la técnica de reemplazo de constante por variable. Definimos una variable que sustituirá a , proponemos una invariante y nuestra guarda para el ciclo:
La estructura que debemos seguir es la siguiente:
Para la inicialización , partimos de , queremos partir del primer elemento del arreglo:
Por lo tanto, . Ahora podemos derivar probando con , queremos aumentar la posición a indexar:
Hemos llegado a una expresión no programable, pero podemos definir una nueva variable tal que:
Analizando la expresión, esta calcula la cantidad de veces que se repite un elemento en el arreglo . Necesitamos un ciclo que incremente la constante y dentro de ese mismo ciclo, otro ciclo que recorra el arreglo y compare los elementos con , un ciclo anidado.
Antes de eso, podemos seguir derivando el cuerpo del ciclo :
Ahora si, para este ciclo anidado podemos aplicar la técnica de reemplazo de constante por variable. Donde la constante es y la nueva variable es . Definimos una invariante y una guarda :
Nuestro problema ahora mismo radica en definir el valor de mediante un ciclo anidado. Tenemos un programa similar al siguiente:
Por la derivación anterior tenemos que en el cuerpo de ciclo . Ahora nos interesa derivar la inicialización y el cuerpo del ciclo anidado .
Partimos de la inicialización con , queremos partir del primer elemento del arreglo:
Por lo tanto, . Ahora podemos derivar probando con , queremos aumentar la posición a indexar:
Hemos llegado a una expresión programable mediante el uso de bloques condicionales. Nuestro programa final sería el siguiente:
Verificación (PI)
La verificación de un programa imperativo puede ser llevada a cabo mediante la Terna de Hoare o mediante la Weakest Precondition. Aplicando cualquiera de ambas, comprobamos si el programa cumple con las condiciones dadas.