Bonjour à tous,
J'ai récemment travaillé à ajouter l'élimination dépendante à Typer (4c632a1f, 8a7b7910), qui repose sur des égalités dans les expressions `case`. Pour les utiliser, on élimine éventuellement ces égalités avec `Eq.cast` (c'est ce que fait la macro `case_return_`, d'ailleurs). On dirait que dans certains cas de son utilisation (au niveau des types), il serait utile que les appels à `Eq.cast` puissent être réduits dans le calcul des WHNF, ce qui n'est pas le cas en ce moment.
Je me pose quelques questions quant à l'implantation de la WHNF de `Eq.cast`, sur lesquelles j'apprécierais vos commentaires. Aussi, pour rendre la discussion plus concrète, j'ai construit un petit exemple où l'unification échoue quand la réduction bloque sur un appel à `Eq.cast`.
Pour obtenir le code de l'exemple à partir de ce courriel, exécutez :
$ sed -n 's/^\s*> ?(.*)/\1/p' <email.txt >exemple.typer
1. Question
Je me demande comment implanter la WHNF de `Eq.cast`, j'ai l'impression, intuitivement, qu'il suffit de vérifier que son argument `p` est bel et bien convertible à `Eq.refl` (autrement dit, ce n'est pas simplement une variable/hypothèse), auquel cas, il me paraît correct d'ignorer l'argument `f` et de se réduire à l'argument explicite.
Évidemment, si `p` n'est pas convertible à `Eq.refl`, la réduction doit bloquer.
Est-ce que cette approche peut causer des problèmes au niveau théorique?
2. Exemple
Utilisons, au niveau des types, `decideBoolEq`, une fonction que l'on définit avec l'élimination dépendante, pour illustrer le problème.
Petite parenthèse, je vais utiliser le `case` builtin pour simplifier les affichages:
case_ = ##case_;
Prenons le type `Decidable` défini de cette façon:
Decidable = typecons (Decidable (ℓ ::: TypeLevel) (prop : Type_ ℓ)) (yes (p :: prop)) (no (p :: Not prop)); yes = datacons Decidable yes; no = datacons Decidable no;
Définissons `decideBoolEq`, qui prouve la décidabilité de l'égalité de booléens. On se sert des lemmes `true≠false` et `false≠true` qu'on définit au passage.
true≠false (p : Eq true false) = Eq_cast (p := p) (f := lambda tf -> case tf | true => True | false => False) (); % This is unit, an inhabitant of `True = Unit`.
false≠true (p : Eq false true) = true≠false (Eq_comm p);
decideBoolEq : (a : Bool) => (b : Bool) => Decidable (Eq a b); decideBoolEq = lambda (a : Bool) (b : Bool) => case a return (Decidable (Eq a b)) | true => (case b return (Decidable (Eq true b)) | true => yes (p := Eq_refl) | false => no (p := true≠false)) | false => (case b return (Decidable (Eq false b)) | true => no (p := false≠true) | false => yes (p := Eq_refl));
Note: Ici, on utilise la macro `case_return_`, qui insère les bons appels à `Eq.cast`.
Définissons maintenant une méthode d'accès pour obtenir la preuve d'un `Decidable`, qu'il soit vrai ou faux:
getProof : (l : TypeLevel) ≡> (prop : Type_ l) ≡> (d : Decidable prop) -> case d | yes => prop | no => Not prop; getProof d = case d return (case d | yes => prop | no => Not prop) | yes (p := p) => p | no (p := p) => p;
Ces définitions nous permettent de définir un terme qui fait échouer l'élaboration:
this_fails = (getProof (decideBoolEq : Decidable (Eq true true)) : Eq true true);
Voici l'erreur qu'on obtient:
% ./samples/exemple.typer:48:9:Error:(ELAB) Type mismatch (residue)! Context expected: % (Eq[257] ##TypeLevel.z typecons (Bool) (false) (true) true[210] true[210]) % but expression has type: % case (decideBoolEq[1] datacons Bool[211] true datacons Bool[211] true) | no _ => (Not[53] ##TypeLevel.z (Eq[259] ##TypeLevel.z typecons (Bool) (false) (true) true[212] true[212])) | yes _ => (Eq[259] ##TypeLevel.z typecons (Bool) (false) (true) true[212] true[212]) % can't unify: % (Eq[257] ##TypeLevel.z typecons (Bool) (false) (true) true[210] true[210]) % with: % case (decideBoolEq[1] datacons Bool[211] true datacons Bool[211] true) | no _ => (Not[53] ##TypeLevel.z (Eq[259] ##TypeLevel.z typecons (Bool) (false) (true) true[212] true[212])) | yes _ => (Eq[259] ##TypeLevel.z typecons (Bool) (false) (true) true[212] true[212]) % > Call: (getProof[0] ##TypeLevel.z (Eq[257] ##TypeLevel.z typecons (Bool) (false) (true) true[210] true[210]) (decideBoolEq[1] datacons Bool[211] true datacons Bool[211] true)) %
Pourquoi? On doit unifier le type de
getProof (decideBoolEq : Decidable (Eq true true))
qui est
case (decideBoolEq (a := true) (b := true)) | yes => Eq true true | no => Not (Eq true true)
avec `Eq true true`. Pour que cette unification fonctionne, il faut que `decideBoolEq (a := true) (b := true)` se réduise à un constructeur de `Decidable`, en l'occurence `yes`. Or, quand on regarde la première branche de `decideBoolEq`, on se rend compte qu'à cause de l'élimination dépendante, on obtient plutôt un appel à `Eq.cast`. Effectivement, en déboggant, j'obtiens le terme:
% (##Eq.cast % ##TypeLevel.z % ##TypeLevel.z % typecons (Bool) (false) (true) % true[210] % datacons Bool[211] true % (##Eq.refl % ##TypeLevel.z % Bool[211] % datacons Bool[211] true) % lambda (a : typecons (Bool) (false) (true)) -> % (Decidable[7] % ##TypeLevel.z % (Eq[258] ##TypeLevel.z typecons (Bool) (false) (true) a[0] datacons Bool[212] true)) % case datacons Bool[211] true % | false => (##Eq.cast ##TypeLevel.z ##TypeLevel.z typecons (Bool) (false) (true) false[210] datacons Bool[212] true <anon>[0] lambda (b : typecons (Bool) (false) (true)) -> (Decidable[8] ##TypeLevel.z (Eq[259] ##TypeLevel.z typecons (Bool) (false) (true) true[212] b[0])) (no[5] ##TypeLevel.z (Eq[258] ##TypeLevel.z typecons (Bool) (false) (true) true[211] false[210]) true≠false[4])) % | true => (##Eq.cast % ##TypeLevel.z % ##TypeLevel.z % typecons (Bool) (false) (true) % true[211] % datacons Bool[212] true % <anon>[0] % lambda (b : typecons (Bool) (false) (true)) -> % (Decidable[8] ##TypeLevel.z % (Eq[259] ##TypeLevel.z typecons (Bool) (false) (true) true[212] b[0])) % (yes[6] % ##TypeLevel.z % (Eq[258] ##TypeLevel.z typecons (Bool) (false) (true) datacons Bool[212] true datacons Bool[212] true) % (Eq_refl[216] ##TypeLevel.z typecons (Bool) (false) (true) datacons Bool[212] true))))
On constate que le `yes` que l'on cherche se trouve sous deux niveaux de `Eq.cast` à cause des deux niveaux de `case_return_` dans `decideBoolEq`. Ces deux appels à `Eq.cast` devraient pouvoir être réduits!
Jean-Alexandre
Afficher les réponses par date
Question
Je me demande comment implanter la WHNF de `Eq.cast`, j'ai l'impression, intuitivement, qu'il suffit de vérifier que son argument `p` est bel et bien convertible à `Eq.refl` (autrement dit, ce n'est pas simplement une variable/hypothèse), auquel cas, il me paraît correct d'ignorer l'argument `f` et de se réduire à l'argument explicite.
Évidemment, si `p` n'est pas convertible à `Eq.refl`, la réduction doit bloquer.
Est-ce que cette approche peut causer des problèmes au niveau théorique?
Ça ne va pas casser la cohérence de la logique (qui dans notre cas est déjà cassée par le manque de termination-checker, de toute façon), non.
Par contre, ça pose un problème au moins conceptuel et potentiellement pratique dans le sens que ça signifie qu'on ne peut pas faire cette réduction après avoir effacé les argument effaçables.
Si on décide que l'effacement se fait après la vérification de types (et donc que la convertibilité est vérifiée sur des termes avant-effacement), alors cette règle ne pose aucun problème. Si par contre on aimerait faire l'effacement avant la vérification (ce qui donne une notion de convertibilité un peu plus forte vu qu'elle va permet de convertir des termes qui ont des parties effaçable non-convertibles), ça pourrait poser problème.
Ceci dit, peut-être qu'on peut avoir le beurre et l'argent du beurre: en fait, actuellement la notion de convertibilité "forte" (contrôlée par la variable `conv_erase`) est utilisée sans effectuer l'effacement en tant que tel, donc on peut utiliser la règle que tu suggères tout en fermant les yeux sur les arguments effaçables lorsqu'on teste la convertibilité de 2 termes.
La méta-théorie d'un tel système n'est probablement pas très jolie par contre: `conv_erase` nous dirait normalement qqch comme
e₁ ≃ e₂ ------------------------------------------- Eq.cast _ _ _ _ _ e₁ ≃ Eq.cast _ _ _ _ _ e₂
quelque soient les arguments `_`, donc ça nous dit aussi que
Eq.cast _ _ _ _ _ e₁ ≃ Eq.cast _ _ _ _ (Eq.refl _) e₁
et donc qu'on peut *toujours* réduire `Eq.cast` sans même regarder si la preuve est `Eq.refl`, et ça on sait que c'est pas sound :-(
Donc en théorie `conv_erase` est incompatible avec la règle que tu proposes, est-ce que ça s'applique aussi à notre implantation de `conv_p`, je ne sais pas.
Mais, je ne vois pas de bonne alternative, alors: ¡Go for it!
- Exemple
Merci pour l'exemple détaillé. Je ne vois pas de manière satisfaisante de résoudre ce problème autre que de faire la réduction que tu suggères.
Stefan