Une preuve est beaucoup plus difficile dans le monde OOP en raison des effets secondaires, de l'héritage illimité et du fait d' null
être membre de tout type. La plupart des preuves reposent sur un principe d'induction pour montrer que vous avez couvert toutes les possibilités, et ces 3 éléments rendent cela plus difficile à prouver.
Disons que nous implémentons des arbres binaires qui contiennent des valeurs entières (dans un souci de simplification de la syntaxe, je n'y apporterai pas de programmation générique, bien que cela ne changera rien.) Dans ML standard, je définirais cela comme ce:
datatype tree = Empty | Node of (tree * int * tree)
Cela introduit un nouveau type appelé tree
dont les valeurs peuvent se présenter dans exactement deux variétés (ou classes, à ne pas confondre avec le concept OOP d'une classe) - une Empty
valeur qui ne contient aucune information et des Node
valeurs qui portent un triplet dont le premier et le dernier les éléments sont tree
s et dont l'élément central est un int
. L'approximation la plus proche de cette déclaration dans la POO ressemblerait à ceci:
public class Tree {
private Tree() {} // Prevent external subclassing
public static final class Empty extends Tree {}
public static final class Node extends Tree {
public final Tree leftChild;
public final int value;
public final Tree rightChild;
public Node(Tree leftChild, int value, Tree rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
}
}
Avec la mise en garde que les variables de type Tree ne peuvent jamais l'être null
.
Écrivons maintenant une fonction pour calculer la hauteur (ou la profondeur) de l'arbre, et supposons que nous avons accès à une max
fonction qui renvoie le plus grand de deux nombres:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Nous avons défini la height
fonction par cas - il y a une définition pour les Empty
arbres et une définition pour les Node
arbres. Le compilateur sait combien de classes d'arbres existent et émettrait un avertissement si vous ne définissiez pas les deux cas. L'expression Node (leftChild, value, rightChild)
de la signature de la fonction lie les valeurs du 3-tuple aux variables leftChild
, value
et rightChild
respectivement afin que nous puissions faire référence dans la définition de la fonction. Cela revient à déclarer des variables locales comme celle-ci dans un langage OOP:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
Comment prouver que nous avons mis en œuvre height
correctement? Nous pouvons utiliser l' induction structurelle , qui consiste à: 1. prouver que height
c'est correct dans le ou les cas de base de notre tree
type ( Empty
) 2. en supposant que les appels récursifs à height
sont corrects, prouver que height
c'est correct pour le cas non-base (s ) (lorsque l'arbre est en fait un Node
).
Pour l'étape 1, nous pouvons voir que la fonction retourne toujours 0 lorsque l'argument est un Empty
arbre. Ceci est correct par définition de la hauteur d'un arbre.
Pour l'étape 2, la fonction revient 1 + max( height(leftChild), height(rightChild) )
. En supposant que les appels récursifs retournent vraiment la taille des enfants, nous pouvons voir que cela est également correct.
Et cela complète la preuve. Les étapes 1 et 2 combinées épuisent toutes les possibilités. Notez, cependant, que nous n'avons pas de mutation, pas de null et qu'il existe exactement deux variétés d'arbres. Supprimez ces trois conditions et la preuve devient rapidement plus compliquée, voire impossible.
EDIT: Puisque cette réponse a atteint le sommet, je voudrais ajouter un exemple moins trivial d'une preuve et couvrir l'induction structurelle un peu plus en profondeur. Ci-dessus, nous avons prouvé que si height
retourne , sa valeur de retour est correcte. Cependant, nous n'avons pas prouvé qu'il renvoie toujours une valeur. Nous pouvons également utiliser l'induction structurelle pour le prouver (ou toute autre propriété). Encore une fois, à l'étape 2, nous sommes autorisés à supposer que la propriété détient des appels récursifs tant que les appels récursifs fonctionnent tous sur un enfant direct du arbre.
Une fonction peut ne pas retourner une valeur dans deux situations: si elle lève une exception et si elle boucle pour toujours. Prouvons d'abord que si aucune exception n'est levée, la fonction se termine:
Prouver que (si aucune exception n'est levée) la fonction se termine pour les cas de base ( Empty
). Puisque nous renvoyons inconditionnellement 0, il se termine.
Prouver que la fonction se termine dans les cas non-base ( Node
). Il y a trois appels de fonction ici: +
, max
et height
. Nous le savons +
et nous nous max
terminons parce qu'ils font partie de la bibliothèque standard du langage et ils sont définis de cette façon. Comme mentionné précédemment, nous sommes autorisés à supposer que la propriété que nous essayons de prouver est vraie sur les appels récursifs tant qu'ils opèrent sur des sous-arbres immédiats, donc les appels se height
terminent également.
Cela conclut la preuve. Notez que vous ne pourrez pas prouver la terminaison avec un test unitaire. Il ne reste plus qu'à montrer que height
cela ne lève pas d'exceptions.
- Prouvez que
height
cela ne lève pas d'exceptions sur le cas de base ( Empty
). Renvoyer 0 ne peut pas lever d'exception, nous avons donc terminé.
- Prouvez que
height
cela ne lève pas d'exception sur le cas non-base ( Node
). Supposons encore une fois que nous connaissons +
et max
ne lançons pas d'exceptions. Et l'induction structurelle nous permet de supposer que les appels récursifs ne lanceront pas non plus (car ils opèrent sur les enfants immédiats de l'arbre). Mais attendez! Cette fonction est récursive, mais pas récursive de queue . On pourrait faire sauter la pile! Notre tentative de preuve a révélé un bogue. Nous pouvons le corriger en changeant height
pour être récursif de queue .
J'espère que cela montre que les preuves ne doivent pas être effrayantes ou compliquées. En fait, chaque fois que vous écrivez du code, vous avez construit de manière informelle une preuve dans votre tête (sinon, vous ne seriez pas convaincu que vous venez d'implémenter la fonction.) En évitant la mutation nulle, inutile et l'héritage illimité, vous pouvez prouver que votre intuition est corriger assez facilement. Ces restrictions ne sont pas aussi sévères que vous pourriez le penser:
null
est un défaut de langage et le supprimer est inconditionnellement bon.
- La mutation est parfois inévitable et nécessaire, mais elle est nécessaire beaucoup moins souvent que vous ne le pensez - en particulier lorsque vous avez des structures de données persistantes.
- Quant à avoir un nombre fini de classes (au sens fonctionnel) / sous-classes (au sens de la POO) vs un nombre illimité d'entre elles, c'est un sujet trop grand pour une seule réponse . Qu'il suffise de dire qu'il y a un compromis de conception là-bas - prouvabilité de l'exactitude vs flexibilité de l'extension.