Vou dar um exemplo mais detalhado de como usar condições pré / pós e invariantes para desenvolver um loop correto. Juntas, essas afirmações são chamadas de especificação ou contrato.
Não estou sugerindo que você tente fazer isso para cada loop. Mas espero que você ache útil ver o processo de pensamento envolvido.
Para isso, traduzirei seu método em uma ferramenta chamada Microsoft Dafny , projetada para provar a exatidão de tais especificações. Ele também verifica o término de cada loop. Observe que o Dafny não possui um for
loop, então tive que usar um while
loop.
Por fim, mostrarei como você pode usar essas especificações para projetar uma versão, indiscutivelmente, um pouco mais simples do seu loop. Esta versão mais simples do loop, de fato, tem a condição do loop j > 0
e a atribuição array[j] = value
- como foi sua intuição inicial.
Dafny nos provará que esses dois loops estão corretos e fazem a mesma coisa.
Farei então uma afirmação geral, com base na minha experiência, sobre como escrever um loop reverso correto, que talvez o ajude a enfrentar essa situação no futuro.
Parte Um - Escrevendo uma Especificação para o Método
O primeiro desafio que enfrentamos é determinar o que o método realmente deve fazer. Para esse fim, projetei condições pré e pós que especificam o comportamento do método. Para tornar a especificação mais exata, aprimorei o método para fazê-lo retornar o índice onde value
foi inserido.
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 especificação captura totalmente o comportamento do método. Minha principal observação sobre essa especificação é que seria simplificado se o procedimento passasse o valor em rightIndex+1
vez de rightIndex
. Mas como não consigo ver de onde esse método é chamado, não sei que efeito essa mudança teria no restante do programa.
Parte Dois - determinando um loop invariável
Agora temos uma especificação para o comportamento do método, temos que adicionar uma especificação do comportamento do loop que convencerá Dafny de que a execução do loop será encerrada e resultará no estado final desejado de array
.
O seguinte é o seu loop original, traduzido para a sintaxe do Dafny, com os invariantes do loop adicionados. Também mudei para retornar o índice onde o valor foi inserido.
{
// 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
}
Isso verifica em Dafny. Você pode vê-lo seguindo este link . Portanto, seu loop implementa corretamente a especificação de método que escrevi na parte um. Você precisará decidir se essa especificação de método é realmente o comportamento que você deseja.
Observe que Dafny está produzindo uma prova de correção aqui. Essa é uma garantia de correção muito mais forte do que a que pode ser obtida pelo teste.
Parte três - um loop mais simples
Agora que temos uma especificação de método que captura o comportamento do loop. Podemos modificar com segurança a implementação do loop, mantendo a confiança de que não alteramos o comportamento do loop.
Modifiquei o loop para que ele corresponda às suas intuições originais sobre a condição do loop e o valor final de j
. Eu argumentaria que esse loop é mais simples que o loop que você descreveu na sua pergunta. É mais frequentemente capaz de usar j
do que j+1
.
Comece j em rightIndex+1
Altere a condição do loop para j > 0 && arr[j-1] > value
Altere a atribuição para arr[j] := value
Reduza o contador do loop no final do loop em vez do início
Aqui está o código. Observe que os invariantes de loop também são um pouco mais fáceis de escrever agora:
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 Quatro - conselhos sobre loop reverso
Depois de ter escrito e provado que estava correto em muitos ciclos ao longo de alguns anos, tenho o seguinte conselho geral sobre fazer um loop para trás.
É quase sempre mais fácil pensar e escrever um loop para trás (decrescente) se o decremento for realizado no início do loop e não no final.
Infelizmente, a for
construção do loop em muitos idiomas dificulta isso.
Suspeito (mas não posso provar) que essa complexidade foi o que causou a diferença na sua intuição sobre o que o loop deveria ser e o que realmente precisava ser. Você está acostumado a pensar em loops avançados (incrementais). Quando você deseja escrever um loop para trás (decrescente), tenta criar o loop, tentando reverter a ordem em que as coisas acontecem em um loop para frente (incrementando). Mas, devido à maneira como a for
construção funciona, você negligenciou para reverter a ordem da atribuição e atualização da variável de loop - o que é necessário para uma verdadeira reversão da ordem das operações entre um loop para trás e para frente.
Parte Cinco - bônus
Apenas para completar, eis o código que você obtém se passar rightIndex+1
para o método em vez de rightIndex
. Essa alteração elimina todos os +2
deslocamentos necessários para pensar sobre a correção do loop.
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
é um erro? Eu ficaria mais cauteloso com o fato de você estar acessandoarray[j]
earray[j + 1]
sem primeiro verificar issoarray.length > (j + 1)
.