Sys.command "ocaml -version";;
La question de programmation pour ce texte était donnée à la fin, en page 7 :
« Programmer la reconnaissance des mots bien formés en écriture préfixe sur la signature de l’exemple du texte à l’aide du critère fourni par le théorème 3 page 5. Il est conseillé de représenter le mot à valider sous forme d’un tableau ou d’une liste de caractères. »
Mathématiquement, l'énoncé à implémenter sous forme d'un critère est le suivant :
Pour que la suite de symboles $s_1,\dots,s_n \in \Omega$ soit l'écriture préfixe d'un terme, il faut et il suffit que les $h_p := \sum_{i=1}^{p} (1 - \alpha(s_i))$ vérifient : $$ h_1,\dots,h_{n-1} \leq 0 \; \text{et} \; h_n = 1. $$
Ce critère numérique va être très simple à implémenter.
Le choix des structures de données à utiliser n'est cependant pas évident.
On définit :
a ? b : c
en C, if a then b else c
en OCaml, ou b if a else c
en Python).type symbole_calcul_prop = F | V | Ou | Et | Non | ITE ;;
type formule_calcul_prop = symbole_calcul_prop list ;;
Quelques exemples de formules du calcul propositionnel, écrites sous forme préfixes, bien formées ou non :
let ex_correct = [Ou; Non; Ou; F; V; Ou; Non; V; F];;
let ex_incorrect = [Ou; Non; F; Non];;
let ex_ite = [ITE; V; F; V];;
let ex_vide = [];;
Cette fonction donne l'arité du symbole du calcul propositionnel pris en argument.
Autrement dit, avec les notations du texte : $$ S = \Omega = S_0 \sqcup S_1 \sqcup S_2 \sqcup S_3 \\ \begin{cases} S_0 &= \{ V, F \}, \\ S_1 &= \{ \mathrm{Non} \}, \\ S_2 &= \{ \mathrm{Et}, \mathrm{Ou} \}, \\ S_3 &= \{ \mathrm{ITE} \}. \end{cases} $$
Notez qu'on peut supposer que l'appel à cette fonction d'arité se fait en $\mathcal{O}(1)$ dans les calculs de complexité, puisqu'après compilation, cette fonction sera une simple lecture d'un tableau.
let arite_calcul_prop (s : symbole_calcul_prop) : int =
match s with
| F -> 0
| V -> 0
| Non -> 1
| Ou -> 2
| Et -> 2
| ITE -> 3
;;
Cette fonction prend en argument une liste de symboles l
et une fonction d'arité sur ces symboles et renvoie "vrai" (true
) si l
est l'écriture préfixe d'un terme et "faux" sinon (false
).
Notez que le "et" en Caml est paresseux, i.e., x && y
n'évalue pas y
si x
est false
.
Donc cette fonction peut s'arrêter avant d'avoir parcouru tous les symboles, dès qu'elle trouve un symbole qui contredit le critère sur les hauteurs $h_p$.
Et cela permet aussi d'obtenir une indication sur le premier symbole à contredire le critère, comme implémenté ensuite.
let ecriture_prefixe_valide (l : 'a list) (arite : 'a -> int) : bool =
let rec aux (l : 'a list) (h : int) : bool =
match l with
| [] -> false
| [t] -> ((h + 1 - (arite t)) = 1)
| t :: q ->
let h_suivant = (h + 1 - (arite t)) in
(h_suivant <= 0) && (aux q h_suivant)
in aux l 0
;;
On vérifie tout de suite sur les 4 exemples définis ci-dessus :
let _ = ecriture_prefixe_valide ex_correct arite_calcul_prop;; (* true *)
let _ = ecriture_prefixe_valide ex_incorrect arite_calcul_prop;; (* false *)
let _ = ecriture_prefixe_valide ex_ite arite_calcul_prop;; (* true *)
let _ = ecriture_prefixe_valide ex_vide arite_calcul_prop;; (* false *)
Avec la remarque précédente, on peut écrire une fonction très similaire, mais qui donnera une indication sur la position (i.e., l'indice) du premier symbole qui fait que le mot n'est pas bien équilibré, si le mot n'est pas bien formé. Si le mot est bien formé, None
est renvoyé.
let ecriture_prefixe_valide_info (l : 'a list) (arite : 'a -> int) : int option =
let rec aux (l : 'a list) (compteur : int) (h : int) : int option =
match l with
| [] -> Some compteur
| [t] ->
if (h + 1 - (arite t)) = 1 (* h = arite(t) *)
then None
else Some compteur
| t :: q ->
(* h est l'accumulateur qui contient la somme des 1 - arite(t) *)
let h_suivant = (h + 1 - (arite t)) in
if h_suivant > 0 then
Some compteur
else
aux q (compteur + 1) h_suivant
in
aux l 0 0
;;
On vérifie tout de suite sur les 4 exemples définis ci-dessus :
let _ = ecriture_prefixe_valide_info ex_correct arite_calcul_prop;; (* None *)
let _ = ecriture_prefixe_valide_info ex_incorrect arite_calcul_prop;; (* Some 3 *)
let _ = ecriture_prefixe_valide_info ex_ite arite_calcul_prop;; (* None *)
let _ = ecriture_prefixe_valide_info ex_vide arite_calcul_prop;; (* Some 0 *)
Cela permet de voir que sur la formule "$\mathrm{Ou} \; \mathrm{Non} \; F \; \mathrm{Non}$", le premier symbole à poser problème est le dernier symbole. Et effectivement, le critère est vérifié jusqu'au dernier symbole $\mathrm{Non}$.
Avec les mêmes exemples :
let _ = ecriture_prefixe_valide ex_correct arite_calcul_prop;; (* true *)
let _ = ecriture_prefixe_valide ex_incorrect arite_calcul_prop;; (* false *)
let _ = ecriture_prefixe_valide ex_ite arite_calcul_prop;; (* true *)
let _ = ecriture_prefixe_valide ex_vide arite_calcul_prop;; (* false *)
On peut aussi transformer un peu la deuxième formule pour la rendre valide.
let ex_correct_2 = [Ou; Non; F; V];;
let _ = ecriture_prefixe_valide ex_correct_2 arite_calcul_prop;; (* true *)
L'objectif est de construire la fonction d'évaluation d'un terme en écriture postfixe présentée dans le texte (Algorithme 2, page 4) :
La pile utilisée dans l'algorithme sera implémentée par une simple liste.
Ce que le texte appelle "valeur" et "omegabarre" sont regroupés dans une liste de couples tels que le premier élément du couple est un symbole s
et le deuxième la fonction f_s
permettant d'interpréter ce symbole ; on considère que les constante sont des fonctions d'arité 0. Cette fonction f_s
prend en arguments une liste d'éléments du domaine et renvoie un élément du domaine.
On a besoin de savoir dépiler plus d'une valeur, pour récupérer les $k$ valeurs successives à donner à l'interprétation d'un symbole d'arité $k \geq 1$.
Cette fonction renvoie un couple de listes :
k
éléments en sommet de la pile p
de sorte que le sommet de la pile p
se trouve en dernière position dans cette liste,p
une fois qu'on a dépilé ses k
éléments du sommet.let depile (k : int) (p : 'a list) : ('a list * 'a list) =
let rec aux (k : int) (p : 'a list) (sommet_pile : 'a list) : ('a list * 'a list) =
match k with
| 0 -> (sommet_pile, p)
| _ when p = [] ->
failwith "Liste vide"
| i ->
let sommet_modif = (List.hd p) :: sommet_pile in
let p_modif = List.tl p in
aux (i - 1) p_modif sommet_modif
in
aux k p []
;;
Il est absolument crucial de faire au moins un test à ce moment là :
depile 0 [0; 1; 2; 3; 4; 5; 6; 7];;
depile 1 [0; 1; 2; 3; 4; 5; 6; 7];;
depile 3 [0; 1; 2; 3; 4; 5; 6; 7];;
depile 8 [0; 1; 2; 3; 4; 5; 6; 7];;
Ça semble bien fonctionner. Notez que la première liste a été retournée ("renversée"), puisque les valeurs ont été empilées dans le sens inverse lors de leurs lectures.
On peut aussi proposer une implémentation alternative, moins élégante mais plus rapide à écrire, avec des tableaux, et deux appels à Array.sub
pour découper le tableau, et Array.of_list
et Array.to_list
pour passer d'une liste à un tableau puis de deux tableaux à deux listes.
let depile_2 (k : int) (p : 'a list) : ('a list * 'a list) =
let pa = Array.of_list p in
let debut = Array.sub pa 0 k in
let fin = Array.sub pa k ((Array.length pa) - k) in
(List.rev (Array.to_list debut)), (Array.to_list fin)
;;
depile_2 0 [0; 1; 2; 3; 4; 5; 6; 7];;
depile_2 1 [0; 1; 2; 3; 4; 5; 6; 7];;
depile_2 3 [0; 1; 2; 3; 4; 5; 6; 7];;
depile_2 8 [0; 1; 2; 3; 4; 5; 6; 7];;
Cette fonction prend en entrée une liste interpretation
de couples (symbole, fonction interprétant ce symbole), un symbole t
et une liste d'arguments arg
et renvoie f_t(arg)
, où f_t
est la fonction associée au sympole t
via interpretation
.
Avec les notations mathématiques du texte, le symbole t
est $\omega$ et f_t
est $\overline{\omega}$ ("omegabarre" dans l'algorithme).
let interprete (interpretation : ('a * ('d list -> 'd)) list) (t : 'a) (arg : 'd list) : 'd =
(List.assoc t interpretation) arg;;
L'interprétation choisie consiste à évaluer la valeur de vérité d'une formule du calcul propositionnel, avec l'interprétation classique.
Notez que pour faciliter le typage, ces fonctions recoivent la pile, i.e., une liste d'arguments (de taille arbitraire) et s'occupent elles-mêmes de récupérer le sommet de pile et les valeurs suivantes.
Les fonctions échouent si la pile donnée n'est pas assez profonde, bien évidemment.
let interp_V _ = true;;
let interp_F _ = false;;
let interp_Non l = not (List.hd l);;
let interp_Ou l = (List.hd l) || (List.hd (List.tl l));;
let interp_Et l = (List.hd l) && (List.hd (List.tl l));;
let interp_ITE l = if (List.hd l) then (List.hd (List.tl l)) else (List.hd (List.tl (List.tl l)));;
Ensuite, on crée cette liste d'association, qui permet d'associer à un symbole $\omega$ sa fonction $\overline{\omega}$ :
let interp_calcul_prop = [
(V, interp_V); (F, interp_F); (* Arité 0 *)
(Non, interp_Non); (* Arité 1 *)
(Ou, interp_Ou); (Et, interp_Et); (* Arité 2 *)
(ITE, interp_ITE) (* Arité 3 *)
];;
Cette fonction prend une liste l
, de symboles que l'on suppose correspondre à l'écriture postfixe correcte d'un terme, une fonction arite : symbole -> entier
et une liste interpretation
d'interprétation des symboles.
Elle renvoie le résultat de l'évaluation du terme l
pour l'interpretation interpretation
.
L'algorithme est annoncé correct par le théorème 2, non prouvé dans le texte (ça peut être une idée de développement à faire au tableau).
let evalue (l : 'a list) (arite : 'a -> int) (interpretation : ('a * ('d list -> 'd)) list) : 'd =
let rec aux (l : 'a list) (p : 'd list) : 'd =
match l with
| [] -> List.hd p
| t :: q ->
let k = arite t in
let (arguments, p_temp) = depile k p in
let valeur = interprete interpretation t arguments in
let p_fin = valeur :: p_temp
in (aux q p_fin)
in
aux l [];;
Si la lecture de l'arité d'un symbole t
, k = arite t
, est en $\mathcal{O}(1)$, alors l'algorithme evalue
a une complexité en $\mathcal{O}(n)$ où $n$ est le nombre de symbole du terme donné en entrée.
En d'autres termes, l'évaluation d'un terme postfixe par la méthode naïve, utilisée pour la notation polonaise inversée, est linéaire dans la taille du terme. Chouette !
On considère le terme "$\mathrm{Ou} \; \mathrm{Non} \; V \; \mathrm{Ou} \; V \; F$" $= \vee \neg V \vee V F \equiv (\neg V) \vee (V \vee F)$ suivant :
let ex1 = List.rev [ Ou; Non; V; Ou; V; F ];; (* écriture préfixe *)
let _ = evalue ex1 arite_calcul_prop interp_calcul_prop;;
On vient de passer de l'écriture préfixe à postfixe, simplement en inversant l'ordre des termes (List.rev
).
Attention, cela ne fonctionne que si tous les symboles sont symmétriques !
Avec un autre exemple, directement écrit en postfixe, correspondant au terme suivant, qui s'interprête à "faux" (F = false
) :
let ex2 = [F; V; Ou; Non; V; Non; F; Ou; Ou];;
let _ = evalue ex2 arite_calcul_prop interp_calcul_prop;;
On considère un second exemple : l'interprétation choisie consiste à construire l'arbre syntaxique d'une formule du calcul propositionnel.
type arbre =
| FeuilleV | FeuilleF (* Arité 0 *)
| NNon of arbre (* Arité 1 *)
| NEt of arbre * arbre | NOu of arbre * arbre (* Arité 2 *)
| NITE of arbre * arbre * arbre (* Arité 3 *)
;;
let interp_V_a _ = FeuilleV;;
let interp_F_a _ = FeuilleF;;
let interp_Non_a l = NNon (List.hd l);;
let interp_Ou_a l = NOu ( (List.hd l), (List.hd (List.tl l) ) );;
let interp_Et_a l = NEt ( (List.hd l), (List.hd (List.tl l) ) );;
let interp_ITE_a l = NITE ( (List.hd l), (List.hd (List.tl l) ), (List.hd (List.tl (List.tl l) ) ) );;
let interp_calcul_prop_a = [
(V, interp_V_a); (F, interp_F_a); (* Arité 0 *)
(Non, interp_Non_a); (* Arité 1 *)
(Ou, interp_Ou_a); (Et, interp_Et_a); (* Arité 2 *)
(ITE, interp_ITE_a); (* Arité 3 *)
];;
let _ = ex2;;
let _ = evalue ex2 arite_calcul_prop interp_calcul_prop_a;;
Un bonus rapide va être de jouer avec l'API du notebook Jupyter, accessible depuis OCaml via le kernel OCaml-Jupyter, pour afficher joliment le terme en $\LaTeX{}$.
#thread;;
#require "jupyter";;
#require "jupyter.notebook";;
let print_latex (s : string) = Jupyter_notebook.display "text/html" ("$$" ^ s ^ "$$");;
print_latex "\\cos(x)";;
let symbole_to_latex (sym : symbole_calcul_prop) =
match sym with
| ITE -> "\\implies"
| Ou -> "\\vee"
| Et -> "\\wedge"
| Non -> "\\neg"
| V -> "V" | F -> "F"
;;
let formule_to_latex (form : symbole_calcul_prop list) =
String.concat " " (List.map symbole_to_latex form)
;;
print_latex (formule_to_latex ex2);;
Sans prendre en compte la structure d'arbre, c'est très moche !
let rec arbre_to_latex (arb : arbre) =
match arb with
| FeuilleV -> "V"
| FeuilleF -> "F"
| NNon(a) -> "(\\neg " ^ (arbre_to_latex a) ^ ")"
| NEt(a, b) -> "(" ^ (arbre_to_latex a) ^ " \\wedge " ^ (arbre_to_latex b) ^ ")"
| NOu(a, b) -> "(" ^ (arbre_to_latex a) ^ " \\vee " ^ (arbre_to_latex b) ^ ")"
| NITE(a, b, c) -> "(" ^ (arbre_to_latex a) ^ " ? " ^ (arbre_to_latex b) ^ " : " ^ (arbre_to_latex c) ^ ")"
;;
let formule_to_latex2 (form : symbole_calcul_prop list) =
let arb = evalue form arite_calcul_prop interp_calcul_prop_a in
arbre_to_latex arb
;;
let _ = evalue ex2 arite_calcul_prop interp_calcul_prop_a
print_latex (formule_to_latex2 ex2);;
On va considérer ici un second exemple, celui de l'arithmétique de Presburger.
En gros, c'est l'arithmétique avec :
Cst
), dans les entiers positifs $\mathbb{N}$,Let
), sous formes de lettres ici (un seul char
),Eq
), de la forme $x = y$, où $x$ et $y$ sont des constantes ou des variables,type symbole_presburger =
Cst | Let | Eq | PEq | O | A | N | Ex
;;
Avec ces symboles, on définit facilement leur arités.
let arite_presburger (s : symbole_presburger) : int =
match s with
| Cst | Let -> 0
| N -> 1
| Eq | O | A | Ex -> 2
| PEq -> 3
;;
Les formules suivent cette grammaire : $$\phi,\phi' := (x=y)|(x+y=z)|\phi\vee\phi'|\phi\wedge\phi'|\neg\phi|\exists x, \phi$$
Les symboles sont donc :
Not
, d'arité 1,Or
, $\wedge$, noté And
, $=$, noté Equal
, et $\exists$, noté Exists
, d'arités 2,PlusEqual
.A noter que cet exemple nécessite des signatures non homogènes :
Plutôt que de travailler avec des listes de symboles, on définit une structure arborescente pour les formules de l'arithmétique de Presburger.
type entier = int ;;
type lettre = char ;;
type cst = I of entier | L of lettre ;;
type formule_presburger =
| Equal of cst * cst
| PlusEqual of cst * cst * cst
| Or of formule_presburger * formule_presburger
| And of formule_presburger * formule_presburger
| Not of formule_presburger
| Exists of cst * formule_presburger
Ces formules peuvent facilement s'écrire comme un terme en notation préfixes :
let rec formule_vers_symboles form =
match form with
| Equal(L(_), L(_)) -> [Eq; Let; Let]
| Equal(I(_), L(_)) -> [Eq; Cst; Let]
| Equal(L(_), I(_)) -> [Eq; Let; Cst]
| Equal(I(_), I(_)) -> [Eq; Cst; Cst]
| PlusEqual(L(_), L(_), L(_)) -> [PEq; Let; Let; Let]
| PlusEqual(I(_), L(_), L(_)) -> [PEq; Cst; Let; Let]
| PlusEqual(L(_), I(_), L(_)) -> [PEq; Let; Cst; Let]
| PlusEqual(I(_), I(_), L(_)) -> [PEq; Cst; Cst; Let]
| PlusEqual(L(_), L(_), I(_)) -> [PEq; Let; Let; Cst]
| PlusEqual(I(_), L(_), I(_)) -> [PEq; Cst; Let; Cst]
| PlusEqual(L(_), I(_), I(_)) -> [PEq; Let; Cst; Cst]
| PlusEqual(I(_), I(_), I(_)) -> [PEq; Cst; Cst; Cst]
| Or(a, b) -> O :: (formule_vers_symboles a) @ (formule_vers_symboles b)
| And(a, b) -> A :: (formule_vers_symboles a) @ (formule_vers_symboles b)
| Not(a) -> N :: (formule_vers_symboles a)
| Exists(L(_), a) -> [Ex; Let] @ (formule_vers_symboles a)
;;
Cet avertissement est normal, ici on impose que dans
Exists(u, a)
,u
soit nécessairement de la formeL(x)
, i.e., une variable et non une constante.
On peut aussi afficher une formule, en la convertissant vers une chaîne de caractère :
let i = string_of_int ;;
let c car = String.make 1 car ;;
let rec formule_vers_chaine form =
match form with
| Equal(L(x), L(y)) -> (c x) ^ "=" ^ (c y)
| Equal(I(x), L(y)) -> (i x) ^ "=" ^ (c y)
| Equal(L(x), I(y)) -> (c x) ^ "=" ^ (i y)
| Equal(I(x), I(y)) -> (i x) ^ "=" ^ (i y)
| PlusEqual(L(x), L(y), L(z)) -> (c x) ^ "+" ^ (c y) ^ "=" ^ (c z)
| PlusEqual(I(x), L(y), L(z)) -> (i x) ^ "+" ^ (c y) ^ "=" ^ (c z)
| PlusEqual(L(x), I(y), L(z)) -> (c x) ^ "+" ^ (i y) ^ "=" ^ (c z)
| PlusEqual(I(x), I(y), L(z)) -> (i x) ^ "+" ^ (i y) ^ "=" ^ (c z)
| PlusEqual(L(x), L(y), I(z)) -> (c x) ^ "+" ^ (c y) ^ "=" ^ (i z)
| PlusEqual(I(x), L(y), I(z)) -> (i x) ^ "+" ^ (c y) ^ "=" ^ (i z)
| PlusEqual(L(x), I(y), I(z)) -> (c x) ^ "+" ^ (i y) ^ "=" ^ (i z)
| PlusEqual(I(x), I(y), I(z)) -> (i x) ^ "+" ^ (i y) ^ "=" ^ (i z)
| Or(a, b) -> (formule_vers_chaine a) ^ "v" ^ (formule_vers_chaine b)
| And(a, b) -> (formule_vers_chaine a) ^ "^" ^ (formule_vers_chaine b)
| Not(a) -> "~" ^ (formule_vers_chaine a)
| Exists(L(x), a) -> "E" ^ (c x) ^ ", " ^ (formule_vers_chaine a)
;;
Et en bonus, on affiche aussi avec une chaîne en $\LaTeX$ :
let rec formule_vers_latex form =
match form with
| Equal(L(x), L(y)) -> (c x) ^ "=" ^ (c y)
| Equal(I(x), L(y)) -> (i x) ^ "=" ^ (c y)
| Equal(L(x), I(y)) -> (c x) ^ "=" ^ (i y)
| Equal(I(x), I(y)) -> (i x) ^ "=" ^ (i y)
| PlusEqual(L(x), L(y), L(z)) -> (c x) ^ "+" ^ (c y) ^ "=" ^ (c z)
| PlusEqual(I(x), L(y), L(z)) -> (i x) ^ "+" ^ (c y) ^ "=" ^ (c z)
| PlusEqual(L(x), I(y), L(z)) -> (c x) ^ "+" ^ (i y) ^ "=" ^ (c z)
| PlusEqual(I(x), I(y), L(z)) -> (i x) ^ "+" ^ (i y) ^ "=" ^ (c z)
| PlusEqual(L(x), L(y), I(z)) -> (c x) ^ "+" ^ (c y) ^ "=" ^ (i z)
| PlusEqual(I(x), L(y), I(z)) -> (i x) ^ "+" ^ (c y) ^ "=" ^ (i z)
| PlusEqual(L(x), I(y), I(z)) -> (c x) ^ "+" ^ (i y) ^ "=" ^ (i z)
| PlusEqual(I(x), I(y), I(z)) -> (i x) ^ "+" ^ (i y) ^ "=" ^ (i z)
| Or(a, b) -> (formule_vers_latex a) ^ "\\vee" ^ (formule_vers_latex b)
| And(a, b) -> (formule_vers_latex a) ^ "\\wedge" ^ (formule_vers_latex b)
| Not(a) -> "\\neg" ^ (formule_vers_latex a)
| Exists(L(x), a) -> "\\exists " ^ (c x) ^ ", " ^ (formule_vers_latex a)
;;
On peut prendre quelques exemples de formules, et les convertir en liste de symboles. Notez qu'on perd l'information des constantes et des lettres !
Des formules bien formées :
let formule_1 = Exists(L('x'), Equal(L('x'), I(3)));;
let formule_2 = Exists(L('x'), Exists(L('y'), PlusEqual(L('x'), L('y'), I(10))));;
let formule_3 = Exists(L('x'), PlusEqual(L('x'), I(1), I(0)));;
print_endline (formule_vers_chaine formule_1);;
print_endline (formule_vers_chaine formule_2);;
print_endline (formule_vers_chaine formule_3);;
print_latex (formule_vers_latex formule_1);;
print_latex (formule_vers_latex formule_2);;
print_latex (formule_vers_latex formule_3);;
let sy1 = formule_vers_symboles formule_1;;
let sy2 = formule_vers_symboles formule_2;;
let sy3 = formule_vers_symboles formule_3;;
Elles sont évidemment bien formées.
let _ = ecriture_prefixe_valide sy1 arite_presburger;; (* true *)
let _ = ecriture_prefixe_valide sy2 arite_presburger;; (* true *)
let _ = ecriture_prefixe_valide sy3 arite_presburger;; (* true *)
On peut regarder d'autres suites de symboles qui ne sont pas valides.
let sy4 = [Ex; Let; Eq; Let; Eq];;
let sy5 = [Ex; Let; Ex; Let; Eq; Let; Let; Cst];;
let sy6 = [Ex; Let; PEq; Let; Eq; Cst];;
let _ = ecriture_prefixe_valide_info sy4 arite_presburger;; (* Some 4 *)
let _ = ecriture_prefixe_valide_info sy5 arite_presburger;; (* Some 6 *)
let _ = ecriture_prefixe_valide_info sy6 arite_presburger;; (* Some 5 *)
Ça suffit pour cet exemple, on voulait juste montrer une autre utilisation de cette fonction ecriture_prefixe_valide
.
Il serait difficile d'interpréter ces termes, par contre, à cause du prédicat $\exists$...
Je n'ai pas essayé d'en faire plus ici, inutile.
Voilà pour la question obligatoire de programmation :
Et on a essayé de faire un peu plus, en implémentant l'algorithme d'évaluation des termes.
Bien-sûr, ce petit notebook ne se prétend pas être une solution optimale, ni exhaustive.
Merci à Aude et Vlad pour leur implémentation, sur laquelle ce document est principalement basé.