Les types
(que l’on dit simples)
(Fun x -> x) + 1
1 2
(un appliqué à deux)
Ifz (Fun x -> x+1) Then 0 Else 1
Et en effet,
Soit le cas (Fun x -> x)+1
.
Dans par exemple la machine ASEC.
Donc : lancer une exception (erreur détectée).
Ou dans le code compilé :
Donc : calculer n’importe quoi (erreur non-détectée).
De par exemple « 1 2 ».
L’exécution échoue (application de 0
à 1
).
On doit pouvoir détecter cette erreur à l’usine (compilation), avant qu’elle ne se produise chez le client (exécution).
Associer à un terme t (syntaxe) un “vrai” truc t = [[t]].
Vrai veut dire disons,
Simplifions : il existe ℕ et une “vraie” fonction est une fonction de A dans B où A et B sont des ensembles.
On peut donc essayer d’associer :
Les problèmes :
La solution : obliger le programmeur à spécifier le domaine de définition des fonctions.
Une solution radicale, mais qui va fonctionner.
|
Cf. définition de méthode en Java:
Syntaxe abstraite du nouveau PCF (dans T.Ast
).
Mais qu’est-ce que T.Type.t
(T) ?
Pour éviter « 1 2
» et « (Fun x -> x+1)+2
»),
il doit suffire de séparer entiers et fonctions.
Pour autoriser
Et interdire
Il faut être un peu plus détaillé, les deux types de f
Nat -> Nat | (Nat -> Nat) -> Nat |
Les types sont décrits ainsi :
Nat
est un type.
A -> B
est un type.
Autrement dit :
|
|
Ou encore :
C’est le language des types (simples), il n’a pour le moment aucune sémantique.
Idée évidente pour la sémantique : des ensembles.
Selon notre programme de vérifier les types à la compilation, les types ne peuvent pas être des ensembles quelconques.
Ex. Pourquoi pas un type ℕ* ? (Nat*) Ça serait bien commode :
Et bien non,
div 100 (fact 10-100)
demande de prouver que la valeur de
fact 10
est > 100, c’est beaucoup demander à un compilateur.
Tant pis : l’usine (compilateur) peut livrer des programmes qui divisent par zéro.
Écrire Fun (x:A) -> t n’empêche pas les erreurs.
Mais cela permet de définir facilement un sous-ensemble des termes : les termes bien typés.
L’ensemble des termes bien typés est défini par une relation
E ⊢ t:A |
(Le terme t possède le type A dans l’environnement E.)
Dans le cas d’un environnement vide on abrège
t:A |
(Le terme t a le type A.)
Mal typé, car x de type Nat est appliqué (à 2).
Bien typé.
Mal typé, car f de type Nat est appliqué (à n-1). On aurait pu écrire:
|
|
|
|
|
|
|
|
|
Avec E ⊢ t2:Nat prouvé comme suit :
|
N.B. Inutile de calculer 2100.
Ressemble très fortement à une sémantique « à grand pas ».
Pour cela, on l’appelle :
Calculer A tel que t:A est surtout vérifier le type de t, car on ne découvre (infère) pas franchement A.
Avec des règles aussi précises,
C’est ce que nous ferons en TP.
On veut exprimer que si un terme est bien typé (à la compilation), alors il ne se produit pas d’erreurs (de type) lors de l’exécution.
Cela se montre par induction sur la dérivation t:A.
Cela se montre en détaillant les termes bloqués et en montrant qu’ils ne sont pas typables.
Une méthode pas satisfaisante, prouver:
(t:A ∧ t↪v) ⇒ v:A |
Un peu court, car termes qui bouclent et erreurs de type sont confondus.
On ajoute une « valeur » wrong qui exprime une erreur de type, et des règles de propagation.
|
|
|
|
On obtient alors une formulation plus précise, qui entraîne:
(t:A ∧ t↪v) ⇒ v ≠ wrong |
Le typage A:t garantit l’absence d’erreurs de type à l’exécution.
La réciproque est bien entendu fausse.
Cette incomplétude est un prix à payer pour le le typage statique.
Pourquoi ? À cause de l’indécidabilité de l’arrêt : il n’existe pas d’algorithme qui permet de décider si P (bien typé) termine.
Soit P bien typé (et donc exempt d’erreur) et :
Un erreur se produit à l’exécution entraîne P termine (et P s’évalue n différent de zéro).
Si t:A et que t ne contient pas Fix, alors toutes les réductions issues de t terminent (Tait).
Corollaire :
(Fun x -> x x) (Fun x -> x x)
n’est pas typable.
Y t →* t (Y t) |
Pour un exemple, cf poly (Exercice 2.10).
Maintenant que les termes contiennent des types, il devient possible d’associer une fonction des mathématiques à toute fonction PCF.
Commençons par associer (fonction [[·]]) un ensembles à chaque type de PCF.
|
|
(où E1 → E2 est l’ensemble des fonctions de E1 vers E2).
On a alors six règles évidentes.
|
|
|
À t:A on associe [[t]] dans [[A]]. ([[t]] est une valeur sémantique, un elément de ∪A ∈ T [[A]]).
La sémantique des termes ouverts est relative à un environnment sémantique E des variables vers les valeurs sémantiques,
Avec une définition à peine modifiée (qui explicite le type des valeurs sémantiques de l’environnement), on verrait que si t:A, alors [[t]] existe et ∈ [[A]], mais bon.
Toute cette dépense théorique pour des paraphrases en italiques !
Mais, la sémantique triviale laisse de côté deux questions.
La tentation est grande de définir [[Fix (x:A) -> t]] comme point fixe de
[[Fun (x:A) -> t]] |
Mais cela ne va pas se faire tout seul.
En PCF on peut écrire le terme Fix (x:A) -> t (du moment que l’on a [x:A] ⊢ t:A).
On se se préocupe pas de l’existence du point fixe de [[Fun (x:A) -> t]]. Par exemple :
T1 = Fix (x:Nat) -> x+1 |
Or, il n’existe pas d’entier x tel que x = x+1.
Si on revient à la sémantique opérationnelle, on avait :
(Fix (x:A) -> t) → [Fix (x:A) -> t\x]t |
Et donc :
T1 → T1+1 → (T1+1)+1 → ⋯ |
Pour compléter les valeurs sémantiques on donne la valeur ⊥ à ce terme (terme de type Nat, qui ne termine pas).
On a donc posé [[Nat]] = ℕ ∪ {⊥}.
Si on complète les quatres règles des opérations
|
Alors ⊥ est l’unique point fixe de [[Fun (x:Nat) -> x+1]]
De même, [[Fun (x:Nat) -> x]]
(function qui à x entier associe x) avait plusieurs point fixes
0, 1, 2,…
Maintenant, cette fonction (qui à x ∈ [[Nat]] associe x) a un pt. fixe de plus ⊥.
Et c’est celui là que l’on veut pour [[Fix (x:Nat) -> x]]
(termine pas).
Points fixes de [[Fun (x:Nat) -> x+x]] ? ⊥ et 0. Valeur de [[Fix (x:Nat) -> x+x]] ? ⊥
Ordre de Scott sur le domaine [[Nat]].
Ordre faiblement complet ? Oui !
On veut le plus petit point fixe.
Il existe bien pour toutes les fonctions croissantes et continues de [[Nat]] dans [[Nat]].
Ce qui est le cas de [[Fun (x:Nat) -> x+1]] etc.
Définition revue des domaines sémantiques :
L’ordre sur [[A -> B]] est défini point à point, f ≤ g sisi ∀ x ∈ [[A]], f(x) ≤ g(x).
Il y a un plus petit elt : la fonction qui à x ∈ [[A]] associe ⊥B (notée ⊥A -> B).
Cette relation est faiblement complète (permutation de limites).
Tous les termes de type A s’interprètent bien dans [[A]] (continuité).
La valeur de Fix (x:A) -> t est définie comme le plus petit point fixe de [[Fun (x:A) -> t]].
Fonction puisssance de deux :
Noté Fix (p:Nat -> Nat) -> t.
Pour avoir la valeur du terme ci-dessus, on calcule le pppf de Φ = [[Fun (p:Nat) -> t]].
Le plus pppf de Φ est la limite de la suite (ak) = (Φk(⊥Nat -> Nat)).
|
Car par exemple : a6(5) = 2 * a5(4) (en sautant quelques étapes).
Il y a un théorème (pas facile) :
[[t]] = n ⇔t↪n |
Ce théorème s’applique-t-il à l’évaluation en appel par nom ou par valeur ?
Considérer :
Cela vaut [[y]][loop=⊥, y=0] qui vaut 0.
Donc c’est appel par nom.
C’est beau de savoir que l’on programme de vraies fonctions.
À prouver des théorèmes. Par ex.
On ne peut pas programmer en PCF typé, la fonction g qui prend f:Nat -> Nat en argument, et renvoie 0 si f 0 vaut zéro et 1 autrement.
Soit donc g de [[(Nat -> Nat) -> Nat]] telle que ci-dessus.
Considérer f0 = [[Fun x:Nat -> 0]], f1 = [[Fun x:Nat -> 1]], On ⊥Nat -> Nat ≤ f0 et ⊥Nat -> Nat ≤ f1. Soit (g croissante) g(⊥Nat -> Nat) = ⊥ C.Q.F.D.
Bon on joue sur les mots, voir Exercice 5.11 pour un exemple plus sérieux.
Ce document a été traduit de LATEX par HEVEA