Voy a dar un ejemplo más detallado de cómo usar condiciones pre / post e invariantes para desarrollar un bucle correcto. En conjunto, tales afirmaciones se denominan especificación o contrato.
No estoy sugiriendo que intentes hacer esto para cada ciclo. Pero espero que le sea útil ver el proceso de pensamiento involucrado.
Para hacerlo, traduciré su método en una herramienta llamada Microsoft Dafny , que está diseñada para probar la exactitud de tales especificaciones. También verifica la terminación de cada bucle. Tenga en cuenta que Dafny no tiene un for
bucle, por lo que he tenido que usar un while
bucle.
Finalmente, mostraré cómo puede usar tales especificaciones para diseñar una versión, posiblemente, un poco más simple de su bucle. De hecho, esta versión de bucle más simple tiene la condición de bucle j > 0
y la asignación array[j] = value
, como era su intuición inicial.
Dafny nos demostrará que ambos bucles son correctos y hacen lo mismo.
Luego, haré un reclamo general, basado en mi experiencia, sobre cómo escribir el bucle correcto hacia atrás, que tal vez te ayude si te enfrentas a esta situación en el futuro.
Primera parte: escribir una especificación para el método
El primer desafío al que nos enfrentamos es determinar qué se supone que debe hacer el método. Con este fin, diseñé condiciones previas y posteriores que especifican el comportamiento del método. Para que la especificación sea más exacta, he mejorado el método para que devuelva el índice donde value
se insertó.
method insert(arr:array<int>, rightIndex:int, value:int) returns (index:int)
// the method will modify the array
modifies arr
// the array will not be null
requires arr != null
// the right index is within the bounds of the array
// but not the last item
requires 0 <= rightIndex < arr.Length - 1
// value will be inserted into the array at index
ensures arr[index] == value
// index is within the bounds of the array
ensures 0 <= index <= rightIndex + 1
// the array to the left of index is not modified
ensures arr[..index] == old(arr[..index])
// the array to the right of index, up to right index is
// shifted to the right by one place
ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
// the array to the right of rightIndex+1 is not modified
ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
Esta especificación captura completamente el comportamiento del método. Mi observación principal acerca de esta especificación es que se simplificaría si el procedimiento pasara el valor en rightIndex+1
lugar de hacerlo rightIndex
. Pero como no puedo ver desde dónde se llama a este método, no sé qué efecto tendría ese cambio en el resto del programa.
Segunda parte: determinar un bucle invariante
Ahora que tenemos una especificación para el comportamiento del método, tenemos que agregar una especificación del comportamiento del bucle que convencerá a Dafny de que la ejecución del bucle terminará y dará como resultado el estado final deseado de array
.
El siguiente es su ciclo original, traducido a la sintaxis de Dafny con invariantes de ciclo agregados. También lo he cambiado para devolver el índice donde se insertó el valor.
{
// take a copy of the initial array, so we can refer to it later
// ghost variables do not affect program execution, they are just
// for specification
ghost var initialArr := arr[..];
var j := rightIndex;
while(j >= 0 && arr[j] > value)
// the loop always decreases j, so it will terminate
decreases j
// j remains within the loop index off-by-one
invariant -1 <= j < arr.Length
// the right side of the array is not modified
invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
// the part of the array looked at by the loop so far is
// shifted by one place to the right
invariant arr[j+2..rightIndex+2] == initialArr[j+1..rightIndex+1]
// the part of the array not looked at yet is not modified
invariant arr[..j+1] == initialArr[..j+1]
{
arr[j + 1] := arr[j];
j := j-1;
}
arr[j + 1] := value;
return j+1; // return the position of the insert
}
Esto se verifica en Dafny. Puede verlo usted mismo siguiendo este enlace . Entonces su ciclo implementa correctamente la especificación del método que escribí en la primera parte. Deberá decidir si esta especificación del método es realmente el comportamiento que desea.
Tenga en cuenta que Dafny está produciendo una prueba de corrección aquí. Esta es una garantía de corrección mucho más sólida que la que puede obtenerse mediante pruebas.
Parte tres: un bucle más simple
Ahora que tenemos una especificación de método que captura el comportamiento del bucle. Podemos modificar de forma segura la implementación del bucle sin perder la confianza de que no hemos cambiado el comportamiento del bucle.
He modificado el bucle para que coincida con sus intuiciones originales sobre la condición del bucle y el valor final de j
. Yo diría que este ciclo es más simple que el ciclo que describiste en tu pregunta. Es más capaz de usar en j
lugar de j+1
.
Comience j en rightIndex+1
Cambie la condición del bucle a j > 0 && arr[j-1] > value
Cambiar la tarea a arr[j] := value
Disminuya el contador del bucle al final del bucle en lugar de comenzar
Aquí está el código. Tenga en cuenta que los invariantes de bucle también son algo más fáciles de escribir ahora:
method insert2(arr:array<int>, rightIndex:int, value:int) returns (index:int)
modifies arr
requires arr != null
requires 0 <= rightIndex < arr.Length - 1
ensures 0 <= index <= rightIndex + 1
ensures arr[..index] == old(arr[..index])
ensures arr[index] == value
ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
{
ghost var initialArr := arr[..];
var j := rightIndex+1;
while(j > 0 && arr[j-1] > value)
decreases j
invariant 0 <= j <= arr.Length
invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
invariant arr[j+1..rightIndex+2] == initialArr[j..rightIndex+1]
invariant arr[..j] == initialArr[..j]
{
j := j-1;
arr[j + 1] := arr[j];
}
arr[j] := value;
return j;
}
Parte cuatro: consejos sobre bucles hacia atrás
Después de haber escrito y demostrado ser correcto en muchos bucles durante varios años, tengo el siguiente consejo general sobre bucles hacia atrás.
Casi siempre es más fácil pensar y escribir un bucle hacia atrás (decreciente) si el decremento se realiza al principio del bucle en lugar de al final.
Desafortunadamente, la for
construcción del bucle en muchos idiomas hace que esto sea difícil.
Sospecho (pero no puedo probar) que esta complejidad es lo que causó la diferencia en su intuición sobre lo que debería ser el bucle y lo que realmente debía ser. Está acostumbrado a pensar en bucles hacia adelante (incrementales). Cuando desee escribir un bucle hacia atrás (decreciente), intente crear el bucle intentando revertir el orden en que las cosas suceden en un bucle hacia adelante (incremental). Pero debido a la forma en que funciona la for
construcción, descuidó invertir el orden de la asignación y la actualización de la variable de bucle, que es necesaria para una verdadera inversión del orden de operaciones entre un bucle hacia atrás y hacia adelante.
Quinta parte - bonificación
Solo para completar, aquí está el código que obtienes si pasas rightIndex+1
al método en lugar de hacerlo rightIndex
. Estos cambios eliminan todas las +2
compensaciones que de otro modo se requieren para pensar sobre la corrección del bucle.
method insert3(arr:array<int>, rightIndex:int, value:int) returns (index:int)
modifies arr
requires arr != null
requires 1 <= rightIndex < arr.Length
ensures 0 <= index <= rightIndex
ensures arr[..index] == old(arr[..index])
ensures arr[index] == value
ensures arr[index+1..rightIndex+1] == old(arr[index..rightIndex])
ensures arr[rightIndex+1..] == old(arr[rightIndex+1..])
{
ghost var initialArr := arr[..];
var j := rightIndex;
while(j > 0 && arr[j-1] > value)
decreases j
invariant 0 <= j <= arr.Length
invariant arr[rightIndex+1..] == initialArr[rightIndex+1..]
invariant arr[j+1..rightIndex+1] == initialArr[j..rightIndex]
invariant arr[..j] == initialArr[..j]
{
j := j-1;
arr[j + 1] := arr[j];
}
arr[j] := value;
return j;
}
j >= 0
es un error? Sería más cauteloso con el hecho de que está accediendoarray[j]
yarray[j + 1]
sin verificarlo primeroarray.length > (j + 1)
.