Démontrer des théor`emes mathématiques en écrivant des

publicité
Démontrer des théorèmes mathématiques
en écrivant des programmes
Charles Bouillaguet
19 janvier 2016
La semaine dernière, on a vu que dans les années 1970, l’apparition d’une des premières « preuves générées
par ordinateur » a posé le problème de sa validité : est-ce qu’une preuve que personne ne peut vérifier
est valide ?
À la même période, Le développement de l’informatique et des langages de programmation a apporté
un autre éclairage sur la notion de « preuve mathématique ». S’il fallait la résumer par un slogan, on
pourrait dire « preuve = programme ».
Echauffement. Considérons les fonctions ML 1 suivantes :
1. let i x = x
2. let k x y = x
3. let s x y z = x z (y z)
4. let a f x = f x
5. let c f g x = f (g x)
6. let t f x y = f y x
. Question 1: Laquelle de ces fonctions implémente en fait la composition f ◦ g ?
. Question 2: Déterminer leurs types.
Dans les types obtenus, α → β → γ se lit en fait α → (β → γ). En effet, une fonction de deux arguments
de types α et β est en fait une fonction qui prend un argument de type α et qui renvoie... une fonction
qui prend un argument de type β qui renvoie elle-même un résultat de type γ.
Maintenant, on va transformer les types de ces expressions ML en formules logiques : les variables de type
(α, β, . . . ) vont devenir des variables booléennes, et la « flèche de fonction » (→) va devenir l’opérateur
d’implication (⇒).
. Question 3: En supposant que les variables de type sont des variables booléennes, dresser la table de
vérités des formules logiques dérivées des types ci-dessus. Qu’observe-t-on ?
Isomorphisme de Curry-Howard Dans ce poly, une justification raisonnable de ce phénomène est
fournie. De plus, on va voir qu’il est possible de le « renverser » : pour démontrer une formule logique, il
suffit d’écrire un programme ML qui a le bon type !
On a donc la correspondance :
Système de types
Type
Programme
Calcul
Programmer
Théorie logique
Formule logique
Preuve
Simplification des preuves
Prouver des théorèmes !
1. Haskell et CAML sont des langages de la famille ML
1
Cette correspondance a pris le nom d’isomorphisme de Curry-Howard. En effet, cette idée générale
(« programme = preuve ») s’est élaborée au fil de l’observations d’abord par Curry 2 (en 1934 et 1958)
puis par Howard (en 1969) de la correspondance entre des systèmes de preuves et des mécanismes de
calcul.
L’idée générale est qu’une fonction de type S → T est une fonction qui prend une preuve de S et produit
une preuve de T . Tous les autres constructeurs logiques s’expriment de la même façon.
Assistants de preuve La poursuite de cette analogie « programme = preuve » a aboutit à la mise
de points de langages de programmations spécifiques, dédiés non pas à la construction de programmes,
mais à la construction de preuves mathématiques (coq, développé à l’INRIA, est l’un d’entre eux). L’idée
générale est que pour prouver un théorème ϕ, il suffit d’écrire dans ces langages un programme dont le
type correspond à ϕ.
Ensuite, un type-checker vérifie que votre programme a bien le type annoncé. Si c’est oui, félicitation :
vous avez démontré ϕ. Si c’est non : votre « preuve » est fausse, et vous devez la réparer.
Ces programmes de vérification des types sont des assistants de preuve. Leur nom est très mal choisis
parce que la plupart du temps ils vous empêchent de faire votre preuve en vous faisant remarquer tel ou
tel défaut. Mais ils vous aident à faire des preuves correctes.
Du coup, on pourrait se demander laquelle des deux à le plus de chances d’être « correcte » : une preuve
écrite en français 3 et approuvée par un petit comité d’experts de la bonne branche des mathématiques ?
Ou bien une preuve écrite dans un langage de programmation et vérifiée par un algorithme sans état
d’âme ?
En 2016, les assistants de preuve restent des outils difficiles à utiliser. Formaliser dedans des mathématiques même peu sophistiquées, par exemple le programme de Licence, pose des problèmes et nécessite
un gros effort. Démontrer n’importe quoi dedans nécessite une formation de plusieurs mois. En plus, la
quantité de travail humain nécessaire pour faire accepter une preuve à un assistant de preuve est considérablement plus élevée que celle qui est nécessaire pour faire accepter une preuve du même théorème à
un comité de mathématiciens.
Malgré tout, en 2005, une équipe de recherche a réussi un exploit : ils ont réalisé une preuve complète
du théorème des 4 couleurs dans un assistant de preuve. Ceci laisse finalement peu de doutes quand à la
validité du théorème.
Quelle est la différence avec la preuve de départ ? La preuve de départ consistait à exécuter un programme
ad hoc, et à accepter l’idée que si ce programme renvoie « OK », alors le théorème est vrai. Le programme
est assez compliqué (écrit à la main, en assembleur, manipule des graphes, des centaines de cas, etc.). Le
problème c’était qu’il était difficile de vérifier si le programme est correct.
Dans un assistant de preuve moderne, la partie critique est le type-checker, mais c’est un programme plus
simple, et plus court. De mémoire, celui de coq fait 3000 lignes de OCaml. En plus, il n’est pas spécialisé
pour chaque théorème : on peut le vérifier une fois pour toute, et après s’en servir pour démontrer tout
et n’importe quoi. En plus, le fait qu’il soit générique et qu’il serve beaucoup augmente la probabilité
qu’on trouve les bugs qu’il pourrait contenir.
On peut donc dire que le développement de l’informatique a bousculé la notion de « vérité mathématique »
de deux façons. D’une part en posant le problème des preuves générées par ordinateur, et non-vérifiables
à la main. D’autre part, avec les systèmes de vérification de preuves, on peut se demander si une « vraie »
preuve, ce n’est pas plutôt celle qui est vérifiée par ordinateur.
Mais avant de rentrer dans les détails, il faut « réviser » les bases.
1
Logique propositionnelle
Vous êtes familier avec la logique propositionnelle, c’est-à-dire où les variables sont booléennes (les seules
valeurs possibles sont “vrai” ou “faux”). Le fameux problème SAT que vous avez sûrement étudié consiste
à déterminer, étant donné un formule de la logique propositionnelle, une valuation qui la satisfait. Les
SAT-solver sont des programmes efficaces, optimisés depuis des décennies pour résoudre ce problème.
2. Haskell Curry. Oui, oui.
3. Avec les formules habituelles : « les autres cas se traitent de la même façon », « il est évident que ... », « Le soin de
vérifier ces détails est laissé au lecteur », etc.
2
Définition (Valuation)
Si ϕ est une formule de la logique propositionnelle, une « valuation » de ϕ est simplement une
affectation d’une valeur aux variables qui apparaissent dans ϕ.
Quand on s’intéresse aux preuves mathématiques, on a plutôt besoin d’un moyen de tester si une formule
est un théorème, c’est-à-dire si elle est vraie quelles que soient les valeurs des variables (on dit qu’elle est
valide, et parle aussi de « tautologie »).
Il y a un moyen facile de tester si une formule ϕ(x1 , . . . , xn ) est un théorème : il suffit d’essayer les 2n
combinaisons des variables, et de vérifier si on obtient « Vrai » à chaque fois. Ceci demande clairement
un temps exponentiel, et l’inconvénient c’est qu’on ne voit pas trop ce qui peut faire office de certificat.
En fait, il est facile de montrer le que problème Non-Theorem, qui consiste à montrer qu’une formule de
la logique propositionnelle n’est pas un théorème, est NP-complet (puisqu’il suffit d’exhiber une valuation
qui la rend fausse).
. Question 4: Si on a un SAT-solver efficace sous la main, comment peut-on s’en servir pour tester si des
formules de la logique propositionnelle sont des théorèmes ?
Les mathématiciens, eux, sont capables de produire des certificats que des formules logiques sont des
théorèmes : ces certificats sont les preuves mathématiques. Ils sont d’ailleurs capables de le faire pour
des formules qui appartiennent à une logique très riche : les variables peuvent être des ensembles, des
entiers, des objets géométriques, des nombres réels, etc.
A contrario, la logique propositionnelle a un faible pouvoir expressif. En effet, on ne peut pas s’en servir
pour exprimer de grandes vérités mathématiques telles que « il existe une infinité de nombres premiers ».
Pour cela, il faudrait au moins qu’on puisse avoir sous la main des variables entières, et pas seulement
des variables booléennes. Les langages dédiés comme coq ont un système de types très riche, qui permet
d’exprimer les mathématiques usuelles.
Cependant, ce problème n’est pas fondamental pour comprendre la correspondance « Preuve ↔ Programme ». On va voir un système de preuve qui permet de « démontrer » des formules de la logique
propositionnelle.
2
Calcul des séquents
Les mathématiciens ne s’embêtent généralement pas à formaliser le système dans lequel ils essayent
de démonter des théorèmes. Il y a des « règles de raisonnement » qui sont connues de tous, et dont
l’application (correcte) donne des raisonnements valables.
Ce flou artistique est adapté aux êtres humains, mais pas vraiment aux machines. Plusieurs systèmes de
preuves formels ont été conçus au début du XXème siècle. Dans ces systèmes, les règles de raisonnement
sont explicites, on doit les appliquer à la lettre, et on ne doit rien faire d’autre. Du coup ces systèmes
sont bien adapté à un traitement automatisé (c’est d’ailleurs pour cela qu’ils avaient été conçu). Le calcul
des séquents, conçu par Gentzen 4 est le plus pratique (pour des machines).
Définition (Séquent)
Un « séquent » est une expression de la forme :
H1 , . . . , Hm ` C1 , . . . , Cn .
Le séquent est composé de deux séquences (finies) de formules logiques, les hypothèses (à gauche)
et les conclusions (à droite).
Notation : on note généralement les formules par des lettres majuscules (A, B, C, . . . ), et les séquences de formules par des lettres grecques majuscules (∆, Λ, Π, . . . ).
La “signification” intuitive du séquent est
(H1 ∧ · · · ∧ Hm ) =⇒ (C1 ∨ · · · ∨ Cn ),
c’est-à-dire que sous les hypothèses H1 , . . . , Hm au moins une des conclusions C1 , . . . , Cn est vraie.
Le calcul des séquents est un ensemble de règles permettant de construire des preuves.
4. 1909–1945. Devenu nazi pendant les années 1930, il est mort dans un camp soviétique.
3
Définition (Règle)
De manière très (trop) générale, une règle du calcul des séquents s’écrit :
H1
...
H2
C
Hm
(R)
Dans cette règle (qui s’appelle R, comme indiqué à droite), H1 , . . . , Hm et C sont des séquents. Les
Hi sont les prémisses de la règle, tandis que C en est la conclusion.
Si les prémisses sont valides, alors l’application de la règle garantit que la conclusion est valide.
Les preuves dans le calcul des séquents sont des arbres dont les noeuds correspondent à l’application de
règles. Les feuilles de cet arbre sont des règles sans prémisses, c’est-à-dire des règles de la forme :
(R)
C
Il n’est pas très difficile de démontrer que si une formule de la logique proposistionnelle se retrouve à la
racine d’une arbre de preuve bien formé dans le calcul des séquents, alors cette formule est un théorème
(la réciproque est plus délicate, mais vraie aussi).
2.1
Calcul des séquents propositionnel
Le calcul des séquents peut servir à « prouver » des formules dans la logique du premier ordre, qui est
plus puissante que la logique propositionnelle, mais ça en nous concerne pas aujourd’hui.
Le calcul des séquents propositionnel, lui, permet de prouver des formules de la logique propositionnelle.
Il est formé des règles suivantes, qui se répartissent traditionnellement en trois groupes.
Groupe identité. Ces règles font apparaı̂tre (Ax) ou disparaı̂tre (Cut) une formule A
A`A
(Ax)
Γ ` ∆, A
Γ, A ` ∆
Γ`∆
( Cut)
Groupe logique. Donne les règles correspondant aux connecteurs logiques. Le ET et le OU jouent
des rôles symmétriques :
Γ, A, B ` ∆
Γ, A ∧ B ` ∆
Γ ` A, ∆
Γ ` B, ∆
Γ ` A ∧ B, ∆
(∧ ` )
Γ, A ` ∆
Γ, B ` ∆
Γ, A ∨ B ` ∆
Γ ` A, B, ∆
Γ ` A ∨ B, ∆
(∨ ` )
(` ∧ )
(` ∨)
La négation permet de faire passer des formules d’un côté ou de l’autre du séquent :
Γ ` A, ∆
Γ, ¬A ` ∆
Γ, A ` ∆
Γ ` ¬A, ∆
(¬ `)
(` ¬)
On pourrait définir A ⇒ B par ¬A ∨ B, mais on peut aussi introduire directement des règles :
Γ ` A, ∆
Γ, B ` ∆
Γ, A ⇒ B ` ∆
Γ, A ` B, ∆
Γ ` A ⇒ B, ∆
(⇒` )
(`⇒)
. Question 5: Les règles de la négation sont probablement les plus étranges. Justifiez-les.
Groupe structurel. Il s’agit de règle qui n’ont pas de contenu logique à proprement parler, mais qui
sont là parce qu’on suppose que les différentes formules qui composent un séquent sont ordonnés.
L’affaiblissement (Weakening) consiste à retirer des hypothèses, où ajouter des conclusions (« qui peut le
plus peut le moins »), tandis que la contraction et la permutation dupliquent ou réarrangent des formules :
4
Γ`∆
Γ, A ` ∆
Γ`∆
Γ ` B, ∆
(W` )
Γ, A, A ` ∆
Γ, A ` ∆
Γ ` B, B∆
Γ ` B, ∆
(C` )
Γ, A, Π, B ` ∆
Γ, B, Π, A ` ∆
(` W)
(` C)
Γ ` A, ∆, B, Σ
Γ ` B, ∆, A, Σ
(P` )
(` P)
Ces règles permettent de démontrer les formules valides de la logique propositionnelle (où les variables
valent « vrai » ou « faux »), et où il n’y a pas de quantificateurs.
2.2
exemples
De petits dessins valant mieux que de longs discours, voici deux « preuves » :
(Ax)
A`A
` ¬A, A
` A, ¬A
` A ∨ ¬A
(Ax)
A ` A (` W)
A ` B, A
(`⇒)
(Ax)
` (A ⇒ B), A
A`A
(⇒` )
(A ⇒ B) ⇒ A ` A, A
(`C )
(A ⇒ B) ⇒ A ` A
(`⇒ )
` ((A ⇒ B) ⇒ A) ⇒ A
(` ¬ )
(` P )
(` ∨ )
En fait, les arbres de preuves sont très redondants : dans presque tous les cas, si on connaı̂t le dessous
de la règle, alors on connaı̂t aussi le dessus. On pourrait donc retirer presque tout le contenu de l’arbre,
sauf la conclusion, et on serait encore capable de reconstituer ce qui est parti :
Ax
`W
`⇒
Ax
⇒`
`⇒
` (((a ⇒ b) ⇒ a) ⇒ a)
Il y a cependant deux petites exceptions :
— Dans (Cut), on ne connaı̂t pas A, qui est une formule arbitraire.
— Dans (P`) et (`P) on ne connaı̂t pas la position de B.
Donc, si on veut retirer tout ce qui ne sert à rien des arbres de preuve, il faut quand même y faire
figurer les informations ci-dessus. Pour Cut il faut donner la formule A, et pour les permutations, il suffit
d’ajouter un entier qui indique la position de B.
Il faut noter qu’une même formule peut admettre des preuves différentes. Par exemple, la formule
h
i
h
i
a ⇒ (b ∨ c) =⇒ (b ⇒ ¬a) ∧ ¬c ⇒ ¬a
admet (au moins) deux preuves différentes :
5
Ax
Ax
Ax
Ax
Ax
P1 `
` P1
¬`
Ax
⇒`
¬`
`¬
`¬
P1 `
P2 `
` P2
` P1
P1 `
Ax
`∨
⇒`
`∨
P1 `
⇒`
Ax
P2 `
⇒`
∧`
P1 `
`⇒
∧`
`⇒
`⇒
`⇒
En regardant ces arbres, on comprend qu’écrire une telle « preuve » en XML n’est vraiment pas fait pour
les êtres humains... Cependant, écrire un programme qui prend en argument une formule de la logique
propositionnelle ainsi qu’un et un arbre de preuve, et qui vérifie la correction de la « preuve » n’est pas
horriblement difficile.
. Question 6: Quel petit « détail » fait que ceci n’entraine pas automatiquement que le problème Theorem est dans la classe NP N
3
3.1
Corresponsance avec le système de type des langages ML
Processus de typage.
Pour expliquer le mystère révélé dans l’introduction, il nous faut comprendre comment les types sont
calculés. Une des particularités des langages de la famille ML, comme Caml ou Haskell, est en effet un
système de typage, inventé une première fois par J. Roger Hindley en 1969, une deuxième fois par Robin
Milner en (1978). Il a pris le nom de système Hindley-Milner-Damas. Un algorithme efficace, l’algorithme
W, permet de calculer les types sans aucune annotation de la part du programmeur.
Mais en fait, ce qui nous intéresse, ce n’est pas tant comment les types sont calculés, mais comment on
peut justifier le résultat. Si le typeur de Caml nous dit : « oui, cette expression est correctement typable,
et son type est : α → int », il faudrait qu’il puisse fournir un certificat facile à vérifier.
Conformément à toute la littérature sur le sujet, on va utiliser des idées et des notations standard. On
écrit « e : T » pour dire que l’expression de Caml e possède le type T (on va écrire les types en majuscule).
On appelle ça un « jugement de typage ». Le cas le plus simple est le cas de l’application d’une fonction
à un argument : si f : F1 → F2 et a : F1 , alors (f a) : F2 .
On peut résumer ça par une règle de typage :
u : F1 → F2
uv : F2
v : F1
Le problème, avec cette formulation, c’est qu’il n’y a pas de moyen très clair de déterminer le type des
variables. La solution habituelle consiste à dire que le typage a toujours lieu dans un environnement, qui
fournit les réponses à ce genre de question.
6
Définition (Contexte de typage)
Un contexte de typage Γ est un ensemble fini de liaisons x : F , où les variables x sont distinctes.
Le domaine de Γ est l’ensemble des variables auxquelles un type est lié. On note Γ, ∆ l’union des
deux contextes Γ et ∆ (dont les domaines doivent être disjoints). En particulier, on onte Γ, x : F
l’union de Γ et de la liaison x : F .
Définition (Jugement de typage)
Un jugement de typage Γ ` u : F signifie que l’expression u possède le type F dans le contexte Γ,
qui donne les types des variables libres de u.
Les règles de typage sont :
Γ, x : F ` x : F
(Var)
Γ ` u : F1 → F2
Γ ` v : F1
Γ ` uv : F2
(App)
Γ, y : F1 ` u[x := y] : F2
(Abs)
Γ ` (fun x → u) : F1 → F2
La règle (Abs) peut s’appliquer seulement si y n’appartient pas déjà au domaine de Γ, et si y n’est
pas libre dans u (si une de ces conditions n’est pas respectée, il faut remplacer y par un autre nom de
variable).
Par exemple, reprenons le terme a ci-dessus, et écrivons sa dérivation de type :
(Var)
(Var)
g : S → T, y : S ` g : S → T
g : S → T, y : S ` y : S
(App)
g : S → T, y : S ` g y : T
(Abs)
g : S → T ` (fun x → g x) : S → T
(Abs)
` (fun f → (fun x → f x)) : (S → T ) → S → T
. Question 7: Écrivez la dérivation de type du terme s ci-dessus.
3.2
Relation avec les preuves en calcul des séquents.
En fait, on peut transformer une dérivation du jugement de typage ` e : F en une preuve de la formule
correspondant à F dans le calcul des séquents, en utilisant les transformations suivantes :
Γ, x : F ` x : F
(Var)
F `F
(Ax)
(Ax)
Γ ` u : F1 → F2
Γ ` v : F1
Γ ` uv : F2
Γ ` F1
F2 ` F2
(⇒`)
Γ ` F1 ⇒ F2
Γ, F1 ⇒ F2 ` F2
( Cut)
Γ, Γ ` F2
( C `)
Γ ` F2
(App)
Γ, y : F1 ` u[x := y] : F2
(Abs)
Γ ` (fun x → u) : F1 → F2
Γ, F1 ` F2
Γ ` F1 ⇒ F2
(`⇒)
. Question 8: Ecrivez une preuve de ` (A ⇒ B) ⇒ A ⇒ B en « traduisant » la dérivation du type de la
fonction a.
Par conséquent, si on arrive à trouver un programme Caml qui a le type correspondant à une formule F ,
on a tout simplement trouvé une preuve de F .
3.3
Limitations du système de type de ML
On voit bien qu’avec ça on peut prouver des formules propositionnelles grace à Caml. Mais qu’en est-il
des formules du premier ordre ?
Le problème, c’est qu’on ne peut pas exprimer facilement de valeurs dans le système de type des langages
ML. En effet, on dispose de constructeurs de types, comme list : c’est en quelque sorte une fonction qui
prend un type et qui renvoie un autre type (étant donné le type int, ça renvoie le type int list).
7
Mais on ne dispose pas de constructeurs de types qui prenne en argument une valeur. Par exemple, on
ne peut pas faire un type fixed_array qui prend en argument un entier (disons 5) et qui renvoie un
type fixed_array(5) (le type des tableaux de taille 5).
Des types qui dépendent de valeurs du langage sont appelés des types dépendants. Et le problème, c’est
que d’une part il n’est plus possible de « deviner » automatiquement les types, comme dans Caml. Mais
en plus, il n’est même pas facile (=décidable) de vérifier qu’ils sont corrects, une fois fournis par le
programmeur !
Les « vrais » assistants de preuve (comme Coq) contiennent en fait un langage de programmation fonctionnel avec un système de types beaucoup plus sophistiqué que celui de Caml. Il y a une interface
utilisateur qui aide le programmeur à écrire des programmes ayant le type donné, donc à prouver le
théorème donné.
4
Application : un assistant de preuve jouet
On a vu ci-dessus comment traiter la question de l’implication : une preuve de A → B est en fait une
fonction qui prend en argument une preuve de A et produit une preuve de B.
Et. Qu’en est-il de A ∧ B ? C’est facile : une preuve de A ∧ B est un objet du langage qui contient à
la fois une preuve de A et une preuve de B. Ca peut donc être une paire :
-- Ceci est du code Haskell
data And a b = And (a,b)
(* Ceci est du code OCaml *)
type (’a,’b) et = And of (’a * ’b)
Les fonctions qui produisent une preuve de A ∧ B à partir de preuves de A et B, ou qui « explosent »
une preuve de A ∧ B sont très faciles :
let and_intro x y = (And (x,y))
let and_elim_l (And (x,y)) = x
let and_elim_r (And (x,y)) = y
and_intro x y = (And (x,y))
and_elim_l (And (x,y)) = x
and_elim_r (And (x,y)) = y
Ou. Le cas de A ∨ B est plus difficile. Définir le type des preuves de « A ou B » est facile avec les
« types-somme » :
type (’a,’b) ou = | Or_left of ’a
| Or_right of ’b
data Ou a b = Or_left a | Or_right b
Fabriquer des preuves de A ∨ B est facile :
or_intro_l x = Or_left x
or_intro_r x = Or_right x
let or_intro_l x = Or_left x
let or_intro_r x = Or_right x
Ce qui est plus difficile, c’est de « consommer » une preuve de A ∨ B. L’idée générale est la suivante :
« Si je peux transformer une preuve de A en preuve de C et que je peux transformer une preuve de B et
preuve de C de B, alors je serai toujours capable de transformer une preuve de A ∨ B en preuve de C ».
Cette idée s’incarne là-dedans :
let or_elim f g = function
| Or_left x -> f x
| Or_right y -> g y
or_elim f g (Or_left x) = f x
or_elim f g (Or_right y) = g y
Non. Enfin, la négation n’est pas très simple non plus. En gros dire que ¬A est vrai, c’est dire que
A ne peut pas être vrai. Une preuve de ¬A est donc un programme qui prend une preuve de A et qui
produit... une contradiction ! Une contradiction est une valeur de type bot :
8
type bot = | Bot
data Bot = Bot
L’idée c’est qu’étant donné une contradiction, on peut déduire n’importe quoi. Il nous faut donc une
fonction qui prend en entrée la valeur spéciale Bot et qui peut produire une valeur... de n’importe quel
type. Pour cela, on triche :
let rec bot_elim Bot = bot_elim Bot
bot_elim Bot = bot_elim Bot
Ceci est en effet une fonction qui ne termine pas.
Si on a un moyen de transformer une preuve de A en contradiction, alors on a une preuve de ¬A. Si on
a une preuve de ¬A et une preuve de A, alors on en dérive une contradiction :
type ’a neg = Neg of ’a -> Bot
let neg_elim x (Neg f) = f x
let neg_intro f = Neg f
data Neg ’a = Neg (’a -> Bot)
neg_elim x (Neg f) = f x
neg_intro f = Neg f
9
Téléchargement