Version d'archive
Parfois, même avec un langage de programmation très fortement typé, certaines erreurs vont passer alors qu'on saurait très bien comment les éviter. Il est possible d'attraper toutes les erreurs, à la compilation, grâce aux types dépendants .
Pour le besoin de notre discussion
OCaml
fera très bien l'affaire comme représentant des langages fortement typés.
Après avoir lancé la commande en ligne ocaml on entre ce code dans la console :
type interval = Interval of int * int;;
On peut alors créer des intervalles d'entiers comme l'intervalle [1,2] ou l'intervalle [0,9] :
# Interval(1,2);; - : interval = Interval (1, 2) # Interval(0,9);; - : interval = Interval (0, 9)
Cependant on peut tout aussi bien créer des intervalles invalides comme [2,1] :
# Interval(2,1);; - : interval = Interval (2, 1)
Et ocaml les accepte comme des intervalles valides !
Alors que faire ?
Il existe une solution que l'on appelle la programmation par contrat et qui consiste à cacher l'implémentation du type interval et à lui associer un constructeur make qui vérifie la validité des paramètres fournis.
module Interval : sig (* interface *) type interval val make : int -> int -> interval end = struct (* implémentation *) type interval = Interval of int * int let make a b = (* constructeur *) assert(a<b); (* assertion *) Interval(a,b) (* résultat *) end;;
On créé l'intervalle [1,2] à l'aide de
Interval.make 1 2
Par contre si on créé
Interval.make 2 1
on a une erreur à l'exécution :
Exception: Assert_failure ("", 11, 4) (* 11 et 4 indiquent la ligne et la colonne où l'erreur s'est produite *)
Avantage :
le programme ne construit pas d'intervalle invalide.
Inconvénient : l'erreur n'est pas détectée à la compilation.
Maintenant discutons cet avantage. Certes le programme ne commet pas l'erreur de construire un intervalle invalide. Néanmoins le programme produit tout de même une erreur qui s'avère tout aussi fatale. Car il est trop tard pour corriger cette erreur. Il n'y a aucune contre-mesure possible. Vous pouvez toujours remplir un rapport d'erreur cela n'empêchera pas le programme de mal s'exécuter tant que l'erreur ne sera pas corrigée par le programmeur. Car c'est lui le responsable de l'erreur, pas l'utilisateur.
La programmation par contrat ressemble plus à un mécanisme de signalement qu'un à mécanisme de prévention. Pour faire de la prévention le seul bon moment c'est à la compilation. À l'exécution on ne peut plus ni agir ni réagir. À l'exécution il est trop tard.
Un jour pas si lointain, un programmeur expérimenté m'a dit :
moi : Pourquoi c'est mon allié qui capture ce territoire alors que j'y ai envoyé plus de troupes que lui
lui : J'avais interverti < et > . C'est corrigé. De toute façon je ne vois pas bien quel est le système de typage qui aurait pu attraper ce bogue à la compilation
Dans tous les cas il est impossible de faire la différence puisque < a le même type que >
Hé bien justement ...
Les types dépendants proposent une approche radicalement différente.
Pour le besoin de notre discussion
Coq
fera très bien l'affaire comme représentant des langages à types dépendants.
Après avoir lancé la commande en ligne coqtop on entre ce code dans la console :
Inductive interval : nat -> nat -> Prop := Interval : forall min max, min < max -> interval min max.
À présent essayons de créer l'intervalle [1,2] :
Coq < Eval compute in Interval 1 2. = Interval 1 2 : 1 < 2 -> interval 1 2
Ce que coq nous dit c'est que
Interval 1 2
n'est pas un intervalle mais une fonction du type
1 < 2
vers le type
interval 1 2
. Quel est donc ce mystérieux type
1 < 2
? C'est le type d'une preuve que
1
est
<
que
2
. C'est le type du paramètre manquant pour la construction pleine et entière d'un intervalle [1,2] valide.
Pour fabriquer cette preuve nous chargeons les capacités arithmétiques de base avec
Require Import Arith.Arith_base.
À présent nous disposons de
lt_n_Sn
, c'est une preuve qu'un entier
n
est plus petit que son successeur
S n
.
Nous allons utiliser cet argument à bon escient car
2
est le successeur de
1
. Il s'ensuit que
lt_n_Sn 1
est de type
1 < 2
.
Nous pouvons maintenant construire l'intervalle [1,2] :
Coq < Eval compute in Interval 1 2 (lt_n_Sn 1). = Interval 1 2 (lt_n_Sn 1) : interval 1 2
Cette fois le résultat est bien du type
interval
.
Toutefois notre situation est encore précaire, par exemple comment construire [0,9] alors que 9 n'est pas le successeur de 0 ?
Hé bien nous disposons de lt_0_Sn qui est une preuve que 0 est < qu'un successeur quelconque. Or 9 est le successeur de 8. Il s'ensuit que lt_0_Sn 8 est de type 0 < 9 .
Coq < Eval compute in Interval 0 9 (lt_0_Sn 8). = Interval 0 9 (lt_0_Sn 8) : interval 0 9
Il reste un dernier cas, le cas général, où la borne inférieure n'est pas zéro et où la borne supérieure n'est pas son successeur.
Par exemple [1,9]. Cette fois nous allons devoir faire la preuve à la main.
Commençons par l'énoncer comme un fait lt_1_9 :
Coq < Fact lt_1_9 : 1 < 9.
Coq entre alors en proof mode , il veut bien accepter ce fait si vous lui en fournissez une démonstration. Ce passage de coq en proof mode est visible dans l'invite : elle passe de Coq < à lt_1_9 < .
Fournissez-lui l'argumentaire
repeat constructor.
Puis affirmez
Qed.
(
Quod Erat Demonstrandum
) pour quitter le
proof mode
.
Coq < Fact lt_1_9 : 1 < 9. 1 subgoal ============================ 1 < 9 lt_1_9 < repeat constructor. Proof completed. lt_1_9 < Qed. repeat constructor. lt_1_9 is defined Coq < Eval compute in Interval 1 9 lt_1_9. = Interval 1 9 lt_1_9 : interval 1 9
Jusqu'à présent on a utilisé que des intervalles à bornes constantes.
Bien entendu les intervalles peuvent aussi avoir des bornes variables.
Pour l'illustrer on va démontrer un tout petit théorème qui dit dire que b est strictement borné par a et c est équivalent à affirmer l'existence de 3 intervalles [ a,b ] [ b,c ] et [ a,c ].
On va appeler ce petit théorème bounded_iff_intervals :
Theorem bounded_iff_intervals : forall a b c, a < b < c <-> interval a b /\ interval b c /\ interval a c.
Puis, pour le prouver, on utilise les 5 tactiques qui suivent :
split qui décompose 1 but a ⇔ b en 2 buts a ⇒ b et a ⇐ b , 1 but a < b < c en 2 buts a < b et b < c ou encore 1 but a ⋀ b en 2 buts a et b .
intro H qui transforme la condition d'un but en une hypothèse H .
destruct H qui décompose une hypothèse H .
apply A qui fait valoir un argumentaire A .
exact H qui fait valoir une hypothèse H .
Proof. split. (* -> *) intro H. destruct H as (Hab,Hbc). split. apply (Interval a b Hab). split. apply (Interval b c Hbc). apply (Interval a c (lt_trans a b c Hab Hbc)). (* <- *) intro H. destruct H as (Hab,(Hbc,Hac)). split. destruct Hab. exact H. destruct Hbc. exact H. Qed.
Comme tout logiciel console, coqtop possède son lot de commandes purement utilitaires.
Les commandes en proof mode :
Restart. Reprendre la preuve à son début.
Undo. Défaire la dernière tactique.
Abort All. Abandonner la preuve et quitter le proof mode .
Les commandes top level :
Quit. Quitter coqtop.
Reset Initial. Réinitialiser coqtop.
Check expr . Affiche le type de l'expression.
Sous GTK2+ coqtop est accompagné de l'environnement graphique Coq IDE.
Inductive interval : nat -> nat -> Prop := Interval : forall min max, min < max -> interval min max. Require Import Arith.Arith_base. Eval compute in Interval 1 2 (lt_n_Sn 1). Eval compute in Interval 0 9 (lt_0_Sn 8). Fact lt_1_9 : 1 < 9. repeat constructor. Qed. Eval compute in Interval 1 9 lt_1_9. Theorem bounded_iff_intervals : forall a b c, a < b < c <-> interval a b /\ interval b c /\ interval a c. Proof. split. (* -> *) intro H. destruct H as (Hab,Hbc). split. apply (Interval a b Hab). split. apply (Interval b c Hbc). apply (Interval a c (lt_trans a b c Hab Hbc)). (* <- *) intro H. destruct H as (Hab,(Hbc,Hac)). split. destruct Hab. exact H. destruct Hbc. exact H. Qed.
Le site officiel .
Le manuel de référence .
La librairie standard .
Le calcul des séquents .
Les types dépendants .
Je ne comprends toujours pas le repeat constructor . Pour moi, la preuve de 1 < 9 se fait en passant par chaque intervalle intermédiaire, c'est-à-dire [1,2], [2,3], [3,4], [4,5], [5,6], [6,7] [7,8] et [8,9] dans lesquels on peut à chaque fois prouver que n < S n
Est-ce le sens de repeat constructor ?
SpiceGuid il y a plus de 10 ans
Bien vu.
Le premier constructor. conduit à 2 <= 8 (car < est défini à l'aide de <= )
Puis 2 <= 7 , 2 <= 6 , 2 <= 5 , ... , 2 <= 2 (trivial).
Aka Guymelef il y a plus de 10 ans
Certes l'erreur ne se produit pas à l'exécution mais l'exécution produit tout de même une erreur.
Euh pour moi il y a une erreur de logique dans cette phrase.
SpiceGuid il y a plus de 10 ans
Peut être que la phrase gagnerait à être découpée en deux parties :
Le programme ne commet pas l'erreur de construire un intervalle invalide.
Toutefois le programme produit une erreur qui s'avère tout aussi fatale (irrécupérable en cours d'exécution).
Là c'est un premier jet, un brouillon, je vais tenir compte de vos remarques et de celles des rédacteurs de sites plus spécialisés. Et mon texte sera modifié, remanié.
L'idéal ça serait de la bonne vulgarisation :
Ne pas prendre le lecteur pour un débile (en mettant partout des smileys et en répétant "facile" tous les 20 mots).
Ne pas penser que le lecteur a la science infuse en le renvoyant à des notions ésotériques supposément acquises.
Ne pas choquer les experts avec des approximations qui flirtent avec la contre-vérité.
D'ailleurs ça ferait un bon sujet de débat : Qu'est-ce que de la bonne vulgarisation ? .
© Copyright 2002-2024 Aeriesguard.com - Mentions légales
Aerie's Guard V 7.0 réalisé par Ertaï, designé par Ivaldir, illustré par Izual et Sophie Masure
Ertaï il y a plus de 10 ans