Je vais donner un exemple plus détaillé de la façon d'utiliser les conditions préalables / postérieures et les invariants pour développer une boucle correcte. Ensemble, ces affirmations sont appelées spécification ou contrat.
Je ne suggère pas que vous essayez de faire cela pour chaque boucle. Mais j'espère que vous trouverez utile de voir le processus de réflexion impliqué.
Pour ce faire, je vais traduire votre méthode dans un outil appelé Microsoft Dafny , conçu pour prouver l'exactitude de telles spécifications. Il vérifie également la terminaison de chaque boucle. Veuillez noter que Dafny n'a pas de for
boucle, j'ai donc dû utiliser une while
boucle à la place.
Enfin, je montrerai comment vous pouvez utiliser ces spécifications pour concevoir une version légèrement plus simple de votre boucle. Cette version de boucle plus simple ne comporte pas la condition de boucle j > 0
et l’affectation array[j] = value
- comme ce fut votre intuition initiale.
Dafny prouvera pour nous que ces deux boucles sont correctes et font la même chose.
Je ferai ensuite une déclaration générale, basée sur mon expérience, sur la façon d’écrire une boucle en arrière correcte, qui vous aidera peut-être si vous faites face à cette situation à l’avenir.
Première partie - Rédaction d'une spécification pour la méthode
Le premier défi auquel nous sommes confrontés consiste à déterminer ce que la méthode est réellement censée faire. À cette fin, j'ai conçu des conditions pré et post spécifiant le comportement de la méthode. Pour rendre la spécification plus précise, j'ai amélioré la méthode afin qu'elle renvoie l'index où elle a value
été insérée.
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..])
Cette spécification capture complètement le comportement de la méthode. Mon observation principale à propos de cette spécification est que cela serait simplifié si la procédure était passée à la valeur rightIndex+1
plutôt qu'à rightIndex
. Mais comme je ne vois pas d'où cette méthode est appelée, je ne sais pas quel effet ce changement aurait sur le reste du programme.
Deuxième partie - Déterminer un invariant de boucle
Maintenant que nous avons une spécification pour le comportement de la méthode, nous devons ajouter une spécification du comportement de la boucle qui convaincra Dafny que l’exécution de la boucle se terminera et aboutira à l’état final souhaité array
.
Ce qui suit est votre boucle originale, traduite en syntaxe Dafny avec des invariants de boucle ajoutés. Je l'ai également changé pour retourner l'index où la valeur a été insérée.
{
// 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
}
Cela se vérifie chez Dafny. Vous pouvez le voir vous-même en suivant ce lien . Donc, votre boucle implémente correctement la spécification de méthode que j'ai écrite dans la première partie. Vous devrez décider si cette spécification de méthode correspond réellement au comportement souhaité.
Notez que Dafny produit ici une preuve d'exactitude. C'est une garantie d'exactitude bien plus forte que celle que l'on peut éventuellement obtenir en testant.
Troisième partie - une boucle plus simple
Maintenant que nous avons une spécification de méthode qui capture le comportement de la boucle. Nous pouvons modifier en toute sécurité l'implémentation de la boucle tout en gardant la certitude que nous n'avons pas changé le comportement de la boucle.
J'ai modifié la boucle afin qu'elle corresponde à vos intuitions d'origine concernant la condition de la boucle et la valeur finale de j
. Je dirais que cette boucle est plus simple que celle que vous avez décrite dans votre question. Il est plus souvent capable d'utiliser j
que de j+1
.
Commencez j à rightIndex+1
Changez la condition de boucle en j > 0 && arr[j-1] > value
Changer le devoir en arr[j] := value
Décrémentez le compteur de boucle à la fin de la boucle plutôt qu'au début
Voici le code. Notez que les invariants de boucle sont aussi un peu plus faciles à écrire maintenant:
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;
}
Quatrième partie - conseils sur le bouclage arrière
Après avoir écrit et prouvé correctement de nombreuses boucles sur plusieurs années, j’ai le conseil général suivant sur les boucles arrière.
Il est presque toujours plus facile de penser et d'écrire une boucle arrière (décrémentation) si la décrémentation est effectuée au début de la boucle plutôt qu'à la fin.
Malheureusement, la for
construction de la boucle dans de nombreuses langues rend cela difficile.
Je soupçonne (mais ne peux pas prouver) que cette complexité est à l'origine de la différence d'intuition quant à ce que devrait être la boucle et ce qu'elle devait être en réalité. Vous êtes habitué à penser aux boucles en avant (incrémentation). Lorsque vous voulez écrire une boucle arrière (décrémentation), essayez de créer la boucle en essayant d'inverser l'ordre dans lequel les choses se passent dans une boucle avant (incrémentante). Mais en raison de la façon dont la for
construction fonctionne, vous avez omis d'inverser l'ordre de l'affectation et de mettre à jour la variable de boucle - ce qui est nécessaire pour une véritable inversion de l'ordre des opérations entre une boucle en amont et en aval.
Cinquième partie - bonus
Juste pour compléter, voici le code que vous obtenez si vous passez rightIndex+1
à la méthode plutôt que rightIndex
. Cette modification élimine tous les +2
décalages qui sont par ailleurs nécessaires pour penser à la correction de la boucle.
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
c'est une erreur? Je serais plus méfiant du fait que vous y accédezarray[j]
etarray[j + 1]
sans le vérifier au préalablearray.length > (j + 1)
.