Ce travail pratique vise à produire une représentation en Java des aspects syntaxique et sémantique de la logique du premier ordre. Il est nécessaire pour suivre ce travail d'avoir des connaissances de base en développement Java et optionnellement de savoir utiliser Maven. Il est tout à fait possible également d'utiliser un IDE (Eclipse, IntelliJ IDEA, ...).
1. Projet Java
Ce travail est réalisé dans un environnement Java. Il est conseillé de reprendre le projet Java utilisé pour le TPXX, cependant, Il est possible de réaliser ce travail au sein d'un nouveau projet.
| Exercice 1. |
|
Si un projet Java existant est utilisé, lui ajouter le package fr.utln.logic.firstorder qui permettra d'implanter les classes et interfaces nécessaires à la représentation de la logique du premier-ordre.
Il est également possible de commencer à partir d'un projet pré-configuré avec Maven et disponible ici qui contient déjà le package demandé.
|
2. Formule du premier-ordre
La logique du premier-ordre (ou calcul des prédicats) repose sur la notion d'ensemble de de formules, noté \( \mathcal{L}_{1} \). Cet ensemble est défini par une grammaire qui s'appuie quand à elle sur les notions de variable, de constante, de fonction, de terme , de prédicat, de connecteur (\( \wedge{} \), \( \vee{} \), \( \to{} \), \( \leftrightarrow{} \)) et de quantificateurs (\( \forall{}, \exists{} \)) de la façon suivante:
- Une variable est un terme
- Une constante est un terme
- Une fonction est un terme
- Une fonction lie un certain nombre de termes
- Un prédicat est une formule
- Un prédicat lie un certain nombre de termes
- Si \( F \) est une formule alors \( \neg{}F \) est une formule
- Si \( F \) et \( G \) sont des formules alors \( F\wedge{}G \), \( F\vee{}G \), \( F\to{}G \) et \( F\leftrightarrow{}G \) sont des formules
- Si \( F \) est une formule et \( x \) une variable alors \( \forall{}x\ F \) et \(\exists{}x\ F\) sont des formules
Fournir une représentation de la logique du premier ordre implique d'implanter les notions composants sa grammaire. Mais avant cela, il est nécessaire de définir de façon abstraite une formule.
| Exercice 2. |
|
Dans le package fr.utln.logic.firstorder, ajouter une interface Formula, qui permettra de représenter une formule. Cette interface est pour le moment vide et sera complétée au fur et à mesure de l'avancée de ce travail.
|
2.1. Terme
La notion de terme est l'une des notions de base de la logique du premier ordre. En effet, d'après sa grammaire:
- Une variable est un terme
- Une constante est un terme
- Une fonction est un terme
- Une fonction lie un certain nombre de termes
| Exercice 3. |
|
Dans le projet Java, ajouter un package fr.utln.logic.firstorder.term qui permettra de rassembler les classes et interfaces liées aux termes de la logique du premier-ordre. Ce package devra contenir une interface Term représentant un terme. L'interface Term déclarera la méthode String getName() qui servira à récupérer le nom du terme.
|
Les constantes et les variables sont les termes les plus faciles à représenter car elles ne font que nommer des valeurs.
| Exercice 4. |
|
Dans le package fr.utln.logic.firstorder.term ajouter:
- une classe Var qui implante Term et qui contient
- un constructeur public Var(String name) prenant en paramètre le nom de la variable représentée;
- la surcharge de la méthode public String toString() qui retourne le nom de la variable;
- une classe Constant qui implante Term et qui contient:
- un constructeur public Constant(String name) prenant en paramètre le nom de la constante représentée;
- la surcharge de la méthode public String toString() qui retourne le nom de la constante;
Penser à implanter les méthodes issues de l'interface Term.
|
Une fonction est un terme qui met en relation un ou plusieurs termes afin de produire des valeurs.
| Exercice 5. |
|
Dans le package fr.utln.logic.firstorder.term ajouter une classe Function qui implante l'interface Term et qui contient:
- un champ private final List<Term> arguments qui stocke les arguments de la fonction;
- un constructeur public Function(String name, Term term) qui crée une fonction nommée name dont l'argument unique est term;
- un constructeur public Function(String name, List<Term> terms) qui crée une fonction nommée name dont les arguments sont terms;
- un constructeur public Function(String name, Term... terms) qui crée une fonction nommée name dont les arguments sont terms;
- une méthode public List<Term> getArguments() qui retourne les arguments (termes) de la fonction;
- la surcharge de la méthode public String toString() qui retournera l'affichage textuel de la fonction sous la forme standard f(a1, ..., an).
Penser à implanter les méthodes de l'interface Term.
|
2.2. Prédicat
L'élément logique unitaire de la logique du premier-ordre est le prédicat. Il représente une formule simple mettant en relation différents termes comme l'exprime la grammaire de \( \mathcal{L}_{1} \):
- Un prédicat est une formule
- Un prédicat lie un certain nombres de termes
Un prédicat est également identifié par un nom.
| Exercice 6. |
|
Dans le package fr.utln.logic.firstorder ajouter une classe Predicate qui implante l'interface Formula et qui contient:
- un champ private final List<Term> arguments qui stocke les arguments du prédicat
- un constructeur public Predicate(String name, Term term) qui crée un prédicat nommé name dont l'argument unique est term
- un constructeur public Predicate(String name, List<Term> terms) qui crée un prédicat nommé name dont les arguments sont terms
- un constructeur public Predicate(String name, Term... terms) qui crée un prédicat nommé name dont les arguments sont terms
- une méthode public String getName() qui retourne le nom du prédicat
- une méthode public List<Term> getArguments() qui retourne les arguments (termes) de la prédicat
- la surcharge de la méthode public String toString() qui retournera l'affichage textuel du prédicat sous la forme standard p(a1, ..., an) ou juste p si le prédicat n'a pas d'argument
Rappel: Contrairement à une fonction, un prédicat peut ne pas avoir d'argument (on l'appelle alors prédicat 0-aire ou proposition primitive)
|
Il est temps de tester si les développements réalisés sont fonctionnels.
| Exercice 7. |
|
Ajouter un package fr.utln.logic.firstorder.example au projet et y ajouter une classe Example01 dont le code est le suivant:
package fr.utln.logic.firstorder.example;
import fr.utln.logic.firstorder.Predicate;
import fr.utln.logic.firstorder.term.Constant;
import fr.utln.logic.firstorder.term.Function;
import fr.utln.logic.firstorder.term.Term;
import fr.utln.logic.firstorder.term.Var;
/**
* A first example of first-order logic API.
* @author Julien Seinturier - Université de Toulon / LIS umr CNRS 7020 - <a href="http://web.seinturier.fr">http://web.seinturier.fr</a>
*/
public class Example01 {
/**
* The main method
* @param args the main method arguments
*/
public static void main(String[] args){
Var a = new Var("a");
Var b = new Var("b");
Constant c = new Constant("c");
Term f = new Function("f", a, b);
Term g = new Function("g", a, c);
Term h = new Function("h", b, c);
Predicate p = new Predicate("P", f, g);
Predicate q = new Predicate("Q");
System.out.println("Variables: ");
System.out.println(" a: "+a);
System.out.println(" b: "+b);
System.out.println("Constants: ");
System.out.println(" c: "+c);
System.out.println("Functions: ");
System.out.println(" f: "+f);
System.out.println(" g: "+g);
System.out.println(" h: "+h);
System.out.println("Predicates: ");
System.out.println(" p: "+p);
System.out.println(" q: "+q);
}
}
Le résultat de l'exécution de la classe Example01 attendu est le suivant:
Variables: a: a b: b Constants: c: c Functions: f: f(a, b) g: g(a, c) h: h(b, c) Predicates: p: P(f(a, b), g(a, c)) q: Q
S'il ne s'agit pas du résultat affiché, il faut reprendre les classes des exercices 2 à 6 et les corriger.
|
2.3. Connecteurs
Les termes et les prédicats représentent les bases de la construction d'une formule du premier ordre. Afin de lier des formules entre elle, la logique du premier ordre utilise 5 connecteurs:
- La négation \( \neg \)
- La conjonction \( \wedge \)
- La disjonction \( \vee \)
- L'implication \( \to \)
- L'équivalence \( \leftrightarrow \)
Selon la grammaire de \( \mathcal{L}_{1} \):
- Si \( F \) est une formule alors \( \neg{}F \) est une formule
- Si \( F \) et \( G \) sont des formules alors \( F\wedge{}G \), \( F\vee{}G \), \( F\to{}G \) et \( F\leftrightarrow{}G \) sont des formules
Les formules mises en relation par un connecteur sont appelées opérandes. De façon générale, un connecteur est un objet qui lie un nombre donné d'opérandes entre-elles.
| Exercice 8. |
|
Ajouter au projet un package fr.utln.logic.firstorder.connector et y ajouter une interface Connector qui permet de représenter un connecteur logique et qui déclare la fonction public Formula[] getOperands() qui retourne les opérandes liées par ce connecteur.
|
Les connecteurs de la logique du premier-ordre peuvent être groupés en deux types:
- le connecteur unaire \( \neg \)
- les connecteurs binaires \( \wedge{}, \vee{}, \to{}, \leftrightarrow{}\)
| Exercice 9. |
|
Dans le package fr.utln.logic.firstorder.connector, ajouter:
- une interface UnaryConnector qui:
- étend l'interface Connector
- déclare la méthode Formula getOperand() retournant l'opérande liée à ce connecteur
- une interface BinaryConnector qui:
- étend l'interface Connector
- déclare la méthode Formula getOperandLeft() qui retourne l'opérande gauche du connecteur
- déclare la méthode Formula getOperandRight() qui retourne l'opérande droite du connecteur
- déclare un champ int LEFT = 0 (pour repérer l'opérande gauche)
- déclare un champ int RIGHT = 1 (pour repérer l'opérande droit)
-
|
Les connecteurs de la logique du premier ordre ont pour point commun de gérer des opérandes de la même façon. Par exemple la gestion des opérandes gauche et droite est la même qu'il s'agisse de \( \wedge{},\ \vee{},\ \to{},\ \leftrightarrow{} \). Il est donc intéressant de factoriser la gestion des opérandes dans des classes abstraites dont hériterons les implantations des connecteurs.
| Exercice 10. |
|
Dans le package fr.utln.logic.firstorder.connector, ajouter:
- une classe abstraite AbstractUnaryConnector qui:
- implante l'interface UnaryConnector
- possède un champ private final Formula formula permettant de gérer l'opérande
- possède un constructeur public AbstractUnaryConnector(Formula formula) pour initialiser l'opérande
- une classe abstraite AbstractBinaryConnector qui:
- implante l'interface BinaryConnector
- possède un champ private final Formula[] formulas permettant de gérer les opérandes gauche et droite
- possède un constructeur public AbstractBinaryConnector(Formula left, Formula right) pour initialiser les opérandes
|
Il est maintenant aisé d'implanter les classes représentant les 5 connecteurs de la logique du premier-ordre.
| Exercice 11. |
|
Dans le package fr.utln.logic.firstorder.connector, ajouter:
- une classe Not qui:
- étend la classe AbstractUnaryConnector
- implante l'interface Formula
- possède un constructeur public Not(Formula formula) permettant de construire la négation \( \neg \)formula
- possède une méthode public String toString() renvoyant la représentation textuelle de la formule (indice: \( \neg \) s'écrit en Unicode '\u00AC')
- une classe And qui:
- étend la classe AbstractBinaryConnector
- implante l'interface Formula
- possède un constructeur public And(Formula left, Formula right) permettant de construire la conjonction left \( \wedge \) right
- possède une méthode public String toString() renvoyant la représentation textuelle de la formule (indice: \( \wedge \) s'écrit en Unicode '\u2227')
- une classe Or qui:
- étend la classe AbstractBinaryConnector
- implante l'interface Formula
- possède un constructeur public Or(Formula left, Formula right) permettant de construire la disjonction left \( \vee \) right
- possède une méthode public String toString() renvoyant la représentation textuelle de la formule (indice: \( \vee \) s'écrit en Unicode '\u2228')
- une classe Imp qui:
- étend la classe AbstractBinaryConnector
- implante l'interface Formula
- possède un constructeur public Imp(Formula left, Formula right) permettant de construire l'implication left \( \to \) right
- possède une méthode public String toString() renvoyant la représentation textuelle de la formule (indice: \( \to \) s'écrit en Unicode '\u2192')
- une classe Equiv qui:
- étend la classe AbstractBinaryConnector
- implante l'interface Formula
- possède un constructeur public Equiv(Formula left, Formula right) permettant de construire l'équivalence left \( \leftrightarrow \) right
- possède une méthode public String toString() renvoyant la représentation textuelle de la formule (indice: \( \leftrightarrow \) s'écrit en Unicode '\u2194')
Attention: Penser à bien appeler le constructeur des classes abstraites dans chacun des cas.
|
Les connecteurs étant implantés, il est temps de tester l'API.
| Exercice 12. |
|
Dans le package fr.utln.logic.firstorder.example, ajouter une classe Example02 dont le code est le suivant:
package fr.utln.logic.firstorder.example;
import fr.utln.logic.firstorder.Formula;
import fr.utln.logic.firstorder.Predicate;
import fr.utln.logic.firstorder.connector.*;
import fr.utln.logic.firstorder.term.Function;
import fr.utln.logic.firstorder.term.Term;
import fr.utln.logic.firstorder.term.Var;
/**
* A second example of first-order logic API.
* @author Julien Seinturier - Université de Toulon / LIS umr CNRS 7020 - <a href="http://web.seinturier.fr">http://web.seinturier.fr</a>
*/
public class Example02 {
/**
* The main method
* @param args the main method arguments
*/
public static void main(String[] args){
Var a = new Var("a");
Var b = new Var("b");
Var x = new Var("x");
Var y = new Var("y");
Term f = new Function("f", x, y);
Predicate p = new Predicate("P", a);
Predicate q = new Predicate("Q", b);
Predicate r = new Predicate("R", f);
Not not = new Not(p);
Or or = new Or(r, p);
And and = new And(p, r);
Imp imp = new Imp(r, q);
Equiv equ = new Equiv(q, p);
Formula formula = new Not(new And(new Or(new Or(p, q), r), new Imp(r, q)));
System.out.println("NOT: "+not);
System.out.println(" Operand: "+not.getOperand());
System.out.println();
System.out.println("AND: "+and);
System.out.println(" L operand: "+and.getOperandLeft());
System.out.println(" R operand: "+and.getOperandRight());
System.out.println();
System.out.println("OR : "+or);
System.out.println(" L operand: "+or.getOperandLeft());
System.out.println(" R operand: "+or.getOperandRight());
System.out.println();
System.out.println("IMP: "+imp);
System.out.println(" L operand: "+imp.getOperandLeft());
System.out.println(" R operand: "+imp.getOperandRight());
System.out.println();
System.out.println("EQU: "+equ);
System.out.println(" L operand: "+equ.getOperandLeft());
System.out.println(" R operand: "+equ.getOperandRight());
System.out.println();
System.out.println("Formula: "+formula);
}
}
L'affichage attendu est alors:
NOT: ¬P(a) Operand: P(a)
AND: P(a) ∧ R(f(x, y)) L operand: P(a) R operand: R(f(x, y))
OR : R(f(x, y)) ∨ P(a) L operand: R(f(x, y)) R operand: P(a)
IMP: R(f(x, y)) → Q(b) L operand: R(f(x, y)) R operand: Q(b)
EQU: Q(b) ↔ P(a) L operand: Q(b) R operand: P(a)
Formula: ¬P(a) ∨ Q(b) ∨ R(f(x, y)) ∧ R(f(x, y)) → Q(b)
Si l'affichage n'est pas correct, reprendre les exercices 8, 9, 10 et 11 afin de faire les correction qui s'imposent.
En s'intéressant au code de la dernière formule affichée:
Formula formula = new Not(new And(new Or(new Or(p, q), r), new Imp(r, q)));
et en observant l'affichage produit:
Formula: ¬P(a) ∨ Q(b) ∨ R(f(x, y)) ∧ R(f(x, y)) → Q(b)
que remarque-t-on ? (indice: la formule codée est \(\neg{}((P(a)\vee{}Q(b)\vee{}R(f(x,y)))\wedge{}(R(f(x,y))\to{}Q(b)))\)
|
Avec l'implantation actuelle, l'affichage d'une formule composée de connecteurs peut soulever un problème dû à la priorité des connecteurs. En effet, si par exemple la formule:
$$ \neg{}((P(a)\vee{}Q(b))\wedge{} R(f(x,y))) $$
est instanciée dans la classe Example02 avec le code:
Formula formula2 = new Not(new And(new Or(p, q), r));
alors le code:
formula2.toString();
produira la chaine de caractères
"¬P(a) ∨ Q(b) ∧ R(f(x, y))"
Ce qui correspond à la formule \( \neg{}P(a)\vee{}(Q(b)\wedge{} R(x,y)) \) et non la formule originale \( \neg{}((P(a)\vee{}Q(b))\wedge{} R(f(x,y))) \)
L'affichage de formules strictes (ou tout connecteur binaire est entouré d'une parenthèse) permet de résoudre le problème.
| Exercice 13. |
|
Editer l'interface Formula et y ajouter une méthode String toStringStrict() afin d'indiquer qu'une formule peut être affichée sous forme stricte.
Cette modification entraine maintenant la nécessité d'implanter cette nouvelle méthode dans toutes les classes héritant de Formula, à savoir:
- Predicate
- Neg
- And
- Or
- Imp
- Equiv
Réaliser les implantations nécessaires afin de s'assurer de l'affichage strict d'une formule lors d'un appel à toStringStrict().
Rappel: Les parenthèses à ajouter ne concerne que les connecteurs binaires. L'appel à toStringStrict() pour Predicate ou Neg doit donc pas ajouter de parenthèses.
|
L'affichage strict des formules de la logique du premier ordre peut être maintenant testé.
| Exercice 14. |
|
Editer la classe Example02 et ajouter à la fin de la méthode main (juste après les System.out.println() déjà en place) la ligne:
System.out.println("Strict : "+formula.toStringStrict());
Exécuter la classe Example02 pour vérifier si l'affichage des formules strictes est opérationnel. Le résultat attendu est:
NOT: ¬P(a) Operand: P(a)
AND: P(a) ∧ R(f(x, y)) L operand: P(a) R operand: R(f(x, y))
OR : R(f(x, y)) ∨ P(a) L operand: R(f(x, y)) R operand: P(a)
IMP: R(f(x, y)) → Q(b) L operand: R(f(x, y)) R operand: Q(b)
EQU: Q(b) ↔ P(a) L operand: Q(b) R operand: P(a)
Formula: ¬P(a) ∨ Q(b) ∨ R(f(x, y)) ∧ R(f(x, y)) → Q(b) Strict : ¬(((P(a) ∨ Q(b)) ∨ R(f(x, y))) ∧ (R(f(x, y)) → Q(b)))
|
2.4. Quantificateurs
Les derniers éléments syntaxiques de la logique du premier ordre sont les quantificateurs \( \forall{} \) et \( \exists{} \). Ils permettent d'appliquer la quantification universelle \( \forall \) ou existentielle \( \exists \) d'une variable à une formule. La grammaire de la logique du premier-ordre indique qu'une formule à laquelle s'applique un quantificateur est elle même une formule:
- Si \( F \) est une formule et \( x \) une variable alors \( \forall{}x\ F \) et \(\exists{}x\ F\) sont des formules
D'un point de vue fonctionnel, un quantificateur peut être représenté comme la liaison entre une variable et la formule quantifiée. La nature de la quantification (universelle ou existentielle) n'ayant pas d'impact.
| Exercice 15. |
|
Ajouter au projet le package fr.utln.logic.firstorder.quantifier et y ajouter une interface Quantifier qui représente un quantificateur de la logique du premier ordre. Cette interface contient :
- une méthode Var getVariable() qui retourne la variable associée au quantificateur
- une méthode Formula getFormula() qui retourne la formule quantifiée
|
La structure interne d'un quantificateur (association variable, formule) étant la même qu'il soit universel ou existentiel, une classe abstraite peut être utilisée pour stocker la variable et la formule quantifiée.
| Exercice 16. |
|
Dans le le package fr.utln.logic.firstorder.quantifier, ajouter une classe abstraite AbstractQuantifier qui permet de gérer la variable et la formule associées à un quantificateur quelconque. Cette classe abstraite:
- étend l'interface Quantifier
- possède un champ private final Var variable pour stocker la variable associée
- possède un champ private final Formula formula pour stocker la formule quantifiée
- possède un constructeur public AbstractQuantifier(Variable v, Formula f) qui initialise les champs variable et formula
- implante la méthode getVariable() de Quantifier
- implante la méthode getFormula() de Quantifier
|
A partir des structures définies précédemment, les quantificateurs existentiel (\( \exists{} \)) et universel (\( \forall{} \)) peuvent être représentés comme des classes étant à la fois des Quantifier et des Formula.
| Exercice 17. |
|
Dans le le package fr.utln.logic.firstorder.quantifier, ajouter:
- Une classe Forall qui représente le quantificateur universel \( \forall{} \) et qui:
- étend la classe abstraite AbstractQuantifier
- implante l'interface Formula
- possède un constructeur public Forall(Var variable, Formula formula) (penser à appeler le constructeur parent)
- redéfini la méthode public String toString() en retournant "∀<variable><formula>" (le caractère unicode de \( \forall \) étant '\u2200')
- implante la méthode public String toStringStrict() en retournant "∀<variable>(<formula>)"
- Une classe Exists qui représente le quantificateur existentiel \( \exists{} \) et qui:
- étend la classe abstraite AbstractQuantifier
- implante l'interface Formula
- possède un constructeur public Exists(Var variable, Formula formula) (penser à appeler le constructeur parent)
- redéfini la méthode public String toString() en retournant "∃<variable><formula>" (le caractère unicode de \( \exists \) étant '\u2203')
- implante la méthode public String toStringStrict() en retournant "∀<variable>(<formula>)"
-
|
Tous les composants syntaxique de la logique du premier-ordre ont été implantés. Il est temps de tester la construction d'une formule complète.
| Exercice 18. |
|
Dans le package fr.utln.logic.firstorder.example, ajouter la classe Example03 dont le code est le suivant:
import fr.utln.logic.firstorder.quantifier.ForAll;
import fr.utln.logic.firstorder.term.Function;
import fr.utln.logic.firstorder.term.Term;
import fr.utln.logic.firstorder.term.Var;
/**
* A third example of first-order logic API. This example focuses on quantifiers.
* @author Julien Seinturier - Université de Toulon / LIS umr CNRS 7020 - <a href="http://web.seinturier.fr">http://web.seinturier.fr</a>
*/
public class Example03 {
/**
* The main method
* @param args the main method arguments
*/
public static void main(String[] args){
Var x = new Var("a");
Var y = new Var("b");
Var z = new Var("x");
Term f = new Function("f", x, y);
Predicate p = new Predicate("P", x, y);
Predicate q = new Predicate("Q", z);
Predicate r = new Predicate("R", f);
Formula formula1 = new ForAll(x, new And(new Or(new Or(p, q), r), new Imp(r, q)));
Formula formula2 = new Exists(y, new Or(new And(new And(p, q), r), new Imp(r, q)));
System.out.println("Formula 1: "+formula1);
System.out.println(" Strict : "+formula1.toStringStrict());
System.out.println();
System.out.println("Formula 2: "+formula2);
System.out.println(" Strict : "+formula2.toStringStrict());
}
}
Exécuter la classe, l'affichage attendu sur la console est le suivant:
Formula 1: ∀a P(a, b) ∨ Q(x) ∨ R(f(a, b)) ∧ R(f(a, b)) → Q(x) Strict : ∀a (((P(a, b) ∨ Q(x)) ∨ R(f(a, b))) ∧ (R(f(a, b)) → Q(x)))
Formula 2: ∃b P(a, b) ∧ Q(x) ∧ R(f(a, b)) ∨ R(f(a, b)) → Q(x) Strict : ∃b (((P(a, b) ∧ Q(x)) ∧ R(f(a, b))) ∨ (R(f(a, b)) → Q(x)))
Si ce n'est pas le cas, les exercice 15, 16 et 17 doivent être repris afin de corriger les erreurs.
|
3. Entrées / Sorties
Il est maintenant possible d'instancier une formule du premier-ordre à partir des classes Java du package fr.utln.logic.firstorder, cependant, l'utilisation directe de constructeurs imbriqués peut être non intuitif. Par exemple, la formule:
$$\forall{}e\ (P(e) \to \exists d ( P(d) \wedge \forall x\ ( I(m(x, y), d) \to I(m(f(x), f(y)), e))))$$
est représentée en Java par :
Var e = new Var("e");
Var d = new Var("d");
Var x = new Var("x");
Var y = new Var("y");
Function fx = new Function("f", x);
Function fy = new Function("f", y);
Function mxy = new Function("m", x, y);
Function mfxfy = new Function("m", fx, fy);
Predicate pe = new Predicate("P", e);
Predicate pd = new Predicate("P", d);
Predicate ixyd = new Predicate("I", mxy, d);
Predicate ifxfye = new Predicate("I", mfxfy, e);
Formula f = new ForAll(e, new Imp(pe, new Exists(d, new And(pd, new ForAll(x, new Imp(ixyd, ifxfye))))));
ou encore:
Var e = new Var("e");
Var d = new Var("d");
Var x = new Var("x");
Var y = new Var("y");
Formula f = new ForAll(e, new Imp(new Predicate("P", e), new Exists(d, new And(new Predicate("P", d), new ForAll(x, new Imp(new Predicate("I", new Function("m", x, y), d), new Predicate("I", new Function("m", new Function("f", x), new Function("f", y)), e)))))));
Ces deux formes sont bien trop lourdes pour être réellement utilisable. Il est nécessaire de pouvoir lire des formules depuis une représentation plus concise, comme une représentation textuelle. La faculté à instancier une formule depuis un texte peut être facilement représentée en Java.
| Exercice 19. |
|
Créer un package fr.utln.logic.firstorder.io et y ajouter l'interface FormulaReader qui décrit un objet capable d'instancier une formule du premier-ordre à partir de son expression en texte. Cette interface déclare la méthode:
/**
* Read the given textual representation and instantiate the corresponding {@link Formula formula}.
* @param input the textual representation of the formula
* @return the formula that corresponds to the given input
*/
Formula read(String input);
|
L'interface FormulaReader permet de caractériser une classe permettant de lire et d'instancier une formule du premier ordre depuis un texte sous forme de chaîne de caractère. Cependant, lorsqu'un programme réalise un traitement de données, celles-ci peuvent ne pas respecter un bon format ou être invalides. Une gestion d'erreur est donc indispensable. Une façon simple de lever une erreur lors de la lecture d'un texte consiste à retourner un message d'erreur et la position dans le texte de la portion ayant entrainé l'erreur.
| Exercice 20. |
|
Dans le package fr.utln.logic.firstorder.io, ajouter la classe FormulaReaderException qui représente une erreur dans la lecture d'une formule du premier-ordre depuis un texte. Cette classe a pour code:
package fr.utln.logic.firstorder.io;
import java.io.Serial;
/**
* An exception raised when a textual representation of a formula cannot be read.
* The exception provides the input and the position within the input where the error has been detected.
*/
public class FormulaReaderException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* The input that has been read.
*/
private final String input;
/**
* The position (index of the character) within the input where the error has been detected.
*/
private final int position;
/**
* Create a new parse exception.
* @param message the description of the error
* @param input the input that has been read
* @param position the position (index of the character) within the input where the error has been detected
*/
public FormulaReaderException(String message, String input, int position) {
super(message);
this.input = input;
this.position = position;
}
/**
* Get the input that has been read.
* @return the input that has been read
*/
public String getInput() {
return this.input;
}
/**
* Get the position (index of the character) within the input where the error has been detected.
* @return the position where the error has been detected
*/
public int getPosition() {
return this.position;
}
/**
* Get a message that describes the error and that shows its location within the input using a caret (<code>^</code>).
* @return a message that describes and locates the error
*/
@Override
public String getMessage() {
if (this.input == null) {
return super.getMessage();
}
return super.getMessage() + " at position " + this.position + System.lineSeparator()
+ " " + this.input + System.lineSeparator()
+ " " + " ".repeat(Math.max(0, Math.min(this.position, this.input.length()))) + "^";
}
}
|
Avec l'ajout de la gestion des erreurs, l'interface FormulaReader peut être étendue.
| Exercice 21. |
|
Dans le package fr.utln.logic.firstorder.io, modifier l'interface FormulaReader afin que la méthode read puisse lever l'exception FormulaReaderException.
|
L'instanciation de formules du premier ordre dans la représentation Java à partir d'une donnée textuelle repose est réalisé par un analyseur syntaxique (parser) et repose sur deux phases:
- Le découpage de l'entrée en jetons (token) représentatifs des concepts à représenter
- L'interprétation des jetons (token) pour construire les objets désirés selon une grammaire
Dans le cadre de la logique du premier-ordre, les jetons (tokens) sont les représentations syntaxique de ce qui compose une formule de la logique du premier ordre, c'est à dire:
| Token |
Syntaxe |
Signification |
| LP |
( |
Parenthèse ouvrante |
| RP |
) |
Parenthèse fermante |
| CO |
, |
Séparation de termes |
| ID |
[a-zA-Z][a-zA-Z0-9]*(?:_[0-9]+)? |
Identificateur de variable, de fonction ou de prédicat (chaine de caractère pouvant être indicée) |
| NOT |
¬ ou \u00AC |
Négation logique |
| AND |
∧ ou \u2227 |
Conjonction logique |
| OR |
∨ ou \u2228 |
Disjonction logique |
| IMP |
→ ou \u2192 |
Implication logique |
| EQUIV |
↔ ou \u2194 |
Equivalence logique |
| FORALL |
∀ ou \u2200 |
Quantificateur universel |
| EXIST |
∃ ou \u2203 |
Quantificateur existentiel |
| EOF |
Fin de chaine, de ligne ou de fichier |
Fin de donnée |
Par exemple, la formule:
$$\forall{}e\ (P(e){}\to{}\exists{}d\ (P(d)\wedge{}Q(d)))$$
est représentée sous forme de token par:
FORALL ID LP ID LP ID RP IMP EXISTS ID LP ID LP ID RP AND ID LR ID RP RP RP EOF
La première étapoe dans la construction d'un analyseur syntaxique pour les formules du premier ordre est de définir les tokens utiles.
| Exercice 22. |
|
Dans le package fr.utln.logic.firstorder.io, ajouter la classe FormulaTokenType qui permettra de gérer les différents types de token utiles à la représentation de formules du premier ordre. Le code de la classe est le suivant:
package fr.utln.logic.firstorder.io;
/**
* The types of the tokens that can be produced by a formula lexer.
*/
public enum FormulaTokenType {
/** The negation (¬). */
NOT,
/** The conjunction (∧). */
AND,
/** The disjunction (∨). */
OR,
/** The implication (→). */
IMP,
/** The equivalence (↔). */
EQUIV,
/** The universal quantifier (∀). */
FORALL,
/** The existential quantifier (∃). */
EXISTS,
/** An opening parenthesis: <code>(</code>, <code>[</code> or <code>{</code>. */
LP,
/** A closing parenthesis: <code>)</code>, <code>]</code> or <code>}</code>. */
RP,
/** The argument separator <code>,</code>. */
CO,
/** The separator between quantified variables and their formula: <code>.</code> or <code>:</code>. */
DOT,
/** An identifier (name of a predicate, a function, a variable or a constant). */
ID,
/** The end of the input. */
EOF
}
|
Le type des token n'est pas suffisant pour pouvoir instancier une formule. En effet, si dans une formule un ID est lu, il faut être capable de savoir quelle est la valeur de cet identifiant. Un token est d'un certain type mais doit contenir également plusieurs autres informations relative au texte dans lequel il est trouvé afin de pouvoir reconstruire les objets. Un token est alors défini par:
- Son type (voir exercice 20)
- Le texte d'où il provient
- Sa position dans le texte avec :
- la localisation du premier caractère le définissant
- la localisation du dernier caractère le définissant
| Exercice 23. |
|
Dans le package fr.utln.logic.firstorder.io, ajouter la classe FormulaToken qui permettra de gérer les différents tokens utilisés pour lire une formule du premier ordre. Le code de la classe est le suivant:
package fr.utln.logic.firstorder.io;
/**
* A formula token produced by a lexer.
* @param type the type of the token
* @param text the text of the token (the name for an {@link FormulaTokenType#ID identifier}, the character for a parenthesis)
* @param position the position (index of the first character) of the token within the input
* @param end the position that follows the last character of the token within the input
* @author Julien Seinturier - Université de Toulon / LIS umr CNRS 7020 - <a href="http://web.seinturier.fr">http://web.seinturier.fr</a>
*/
public record FormulaToken(FormulaTokenType type, String text, int position, int end) {
/**
* Create a new token whose end is not known yet (the end is set to the position).
* @param type the type of the token
* @param text the text of the token
* @param position the position (index of the first character) of the token within the input
*/
public FormulaToken(FormulaTokenType type, String text, int position) {
this(type, text, position, position);
}
/**
* Check if this token is immediately followed by the given token (without any character in between).
* @param next the token to check
* @return <code>true</code> if the given token starts exactly where this token ends and <code>false</code> otherwise
*/
public boolean isAdjacentTo(FormulaToken next) {
return this.end == next.position;
}
@Override
public String toString() {
return this.type + (this.text.isEmpty() ? "" : "[" + this.text + "]") + "@" + this.position;
}
}
|
Grâce aux tokens et à la définition formelle d'une formule du premier-ordre, une grammaire de construction de formule peut être exprimée:
formula ::= imp { EQUIV imp }
imp ::= or [ IMP imp ]
or ::= and { OR and }
and ::= unary { AND unary }
unary ::= NOT unary | quantified | primary
quantified ::= ( FORALL | EXISTS ) ID { CO ID } unary
primary ::= LP formula RP | ID [ LP term { CO term } RP ]
term ::= ID [ LP term { CO term } RP ]
Construire une formule revient alors à appliquer cette grammaire à un ensemble de token. C'est le rôle d'un analyseur syntaxique (ou parser).
| Exercice 24. |
|
Dans le package fr.utln.logic.firstorder.io, ajouter la classe FormulaParser qui réalise l'analyse syntaxique d'une formule du premier ordre exprimée sous-forme tokens et qui produit la représentation Java de cette formule. Le but de ce travail n'étant pas la construction d'un analyseur syntaxique, le code de la classe peut être récupéré ici.
|
Il ne manque plus pour lire une formule du premier-ordre depuis un texte d'en extraire les tokens nécessaires. Cette étape est directement liée à la syntaxe sous-jacente et à la façon dont les tokens sont eux-même représentés.
3.1. Unicode
Unicode permet de représenter un grand nombre de caractères spéciaux, dont ceux utilisée par la logique du premier-ordre:
| Symbole |
Caractère |
Code |
Token |
| Négation |
¬ |
\u00AC |
NOT |
| Conjonction |
∧ |
\u2227 |
AND |
| Disjonction |
∨ |
\u2228 |
OR |
| Implication |
→ |
\u2192 |
IMP |
| Equivalence |
↔ |
\u2194 |
EQUIV |
| Quantificateur universel |
∀ |
\u2200 |
FORALL |
| Quantificateur existentiel |
∃ |
\u2203 |
EXISTS |
| Quantificateur existentiel |
( |
\u0028 |
LP |
| Quantificateur existentiel |
) |
\u0029 |
RP |
| Quantificateur existentiel |
, |
\u002C |
CO |
Avec ce codage, la formule
$$\forall{}e\ (P(e) \to \exists d ( P(d) \wedge \forall x\ ( I(m(x, y), d) \to I(m(f(x), f(y)), e))))$$
se représente par la chaine de caractère unicode:
∀e (P(e) → ∃d (P(d) ∧ ∀ x ( I(m(x, y), d) → I(m(f(x), f(y)), e))))
ou encore en utilisant directement les codes:
\u2200 e (P(e) \u2192 \u2203 d (P(d) \u2227 \u2200 x ( I(m(x, y), d) \u2192 I(m(f(x), f(y)), e))))
Il est possible de traiter ces chaines de caractères pour en construire des formules.
| Exercice 25. |
|
Ajouter le package fr.utln.logic.firstorder.io.unicode au projet et ajouter dans celui-ci la classe UnicodeValues qui permettra de gérer les caractères Unicode intéressants pour la représentation de formules du premier ordre. Le code de la classe est le suivant:
package fr.utln.logic.firstorder.io.unicode;
public class UnicodeValues {
public static final char NOT_CH = '\u00AC';
public static final char AND_CH = '\u2227';
public static final char OR_CH = '\u2228';
public static final char IMP_CH = '\u2192';
public static final char EQUIV_CH = '\u2194';
public static final char FORALL_CH = '\u2200';
public static final char EXISTS_CH = '\u2203';
}
|
La création des tokens en fonction du texte lu est le travail d'un analyseur lexical (ou lexer). Celui-ci parcours le texte, détecte les tokens et renseigne les données nécessaires (type, position) afin de pouvoir ensuite affecter les bonnes valeurs aux formules crées (en particulier leurs identifiants).
| Exercice 26. |
|
Dans le package fr.utln.logic.firstorder.io.unicode, ajouter la classe UnicodeLexer qui permettra de découper un texte Unicode en différents tokens utilisés pour construire une formule du premier ordre. Le but de ce travail n'étant pas la construction d'un analyseur lexical, le code de la classe peut être récupéré ici.
|
La lecture d'une formule du premier-ordre exprimée sous-forme de texte Unicode peut enfin être réalisée en instanciant l'interface FormulaReader avec une classe utilisant le lexer unicode et le parser précédemment implantés.
| Exercice 27. |
|
Dans le package fr.utln.logic.firstorder.io.unicode, ajouter la classe UnicodeReader qui implante FormulaReader et permet d'instancier une formule du premier-ordre à partir de son expression en texte Unicode. Le code de la classe est le suivant:
package fr.utln.logic.firstorder.io.unicode;
import fr.utln.logic.firstorder.Formula;
import fr.utln.logic.firstorder.Predicate;
import fr.utln.logic.firstorder.io.*;
import fr.utln.logic.firstorder.term.Constant;
import fr.utln.logic.firstorder.term.Function;
import fr.utln.logic.firstorder.term.Var;
import java.util.*;
/**
* A reader that instantiates a first-order logic {@link Formula formula} from its unicode representation,
*/
public class UnicodeReader implements FormulaReader {
/**
* The names of the identifiers that have to be read as constants.
*/
private final Set<String> constants;
/**
* Create a new unicode reader. Without declared constants, the free identifiers used as terms are read as variables
* (except the numbers that are always read as constants).
*/
public UnicodeReader() {
this.constants = new HashSet<>();
}
/**
* Create a new unicode reader that reads the given free identifiers as {@link Constant constants}.
* @param constants the names of the constants
*/
public UnicodeReader(String... constants) {
this();
declareConstants(constants);
}
@Override
public Formula read(String content) throws FormulaReaderException {
if (content == null)
throw new IllegalArgumentException("Input cannot be null");
List<FormulaToken> tokens = new UnicodeLexer(content).tokenize();
return new FormulaParser(content, tokens, constants).parse();
}
/**
* Declare the given names as {@link Constant constants}. When they are not bound by a quantifier,
* the identifiers with these names are read as constants instead of free variables.
* @param names the names of the constants
* @return this reader
* @throws IllegalArgumentException if a name is <code>null</code> or empty
*/
public UnicodeReader declareConstants(String... names) {
if (names != null) {
for (String name : names) {
if ((name == null) || name.isEmpty())
throw new IllegalArgumentException("Constant cannot have empty name.");
this.constants.add(name);
}
}
return this;
}
/**
* Get the names of the declared {@link Constant constants}.
* @return the names of the declared constants (unmodifiable)
*/
public Set<String> getConstants() {
return Collections.unmodifiableSet(this.constants);
}
}
|
L'API Java permet maintenant de lire des formules du premier ordre exprimées de façon textuelle.
| Exercice 28. |
|
Dans le package fr.utln.logic.firstorder.example, ajouter la classe Example04 qui permet de tester la lecture de formule du premier-ordre depuis une expression textuelle Unicode. Le code de la classe est le suivant:
package fr.utln.logic.firstorder.example;
import fr.utln.logic.firstorder.Formula;
import fr.utln.logic.firstorder.Predicate;
import fr.utln.logic.firstorder.connector.And;
import fr.utln.logic.firstorder.connector.Imp;
import fr.utln.logic.firstorder.io.FormulaReader;
import fr.utln.logic.firstorder.io.unicode.UnicodeReader;
import fr.utln.logic.firstorder.quantifier.Exists;
import fr.utln.logic.firstorder.quantifier.ForAll;
import fr.utln.logic.firstorder.term.Function;
import fr.utln.logic.firstorder.term.Var;
/**
* A third example of first-order logic API. This example focuses on formula I/O.
*/
public class Example04 {
/**
* The main method
* @param args the main method arguments
*/
public static void main(String[] args){
FormulaReader reader = null;
// Unicode
String unicode = "\u2200e (P(e) \u2192 \u2203 d ( P(d) \u2227 \u2200 x ( I(m(x, y), d) \u2192 I(m(f(x), f(y)), e))))";
// Equivalent writing
//String unicode = "∀e (P(e) → ∃ d ( P(d) ∧ ∀ x ( I(m(x, y), d) → I(m(f(x), f(y)), e))))";
reader = new UnicodeReader();
Formula fu = reader.read(unicode);
System.out.println("Unicode");
System.out.println(" Text : \""+unicode+"\"");
System.out.println(" formula: " + fu);
System.out.println(" strict : " + fu.toStringStrict());
System.out.println();
// Loop
Formula fr = reader.read(fu.toStringStrict());
System.out.println("Loop");
System.out.println(" Text : \""+fu.toStringStrict()+"\"");
System.out.println(" formula: " + fr);
System.out.println(" strict : " + fr.toStringStrict());
}
}
|
On remarque dans l'exercice précédent que les constantes doivent être explicitement fournies à la classe permettant de lire une formule depuis un texte. Cette obligation est due au fait qu'il n'est pas possible de savoir en lisant une formule si un identifiant est celui d'une variable ou celui d'une constante. Cette information doit être fournie en amont.
3.2. LaTeX
Sur la même architecture que la lecture de formules depuis un texte Unicode, il est possible de réaliser une implantation de FormulaReader lisant des textes au format LaTeX.
| Exercice 29. |
|
Ajouter au projet le package fr.utln.logic.firstorder.io.latex, et y ajouter les fichiers suivants:
|
L'implantation de la lecture de formules exprimées en LaTeX peut être testée en enrichissant l'Example04.
| Exercice 30. |
|
Dans le package fr.utln.logic.firstorder.example, modifier la classe Example04 avec le code suivant:
package fr.utln.logic.firstorder.example;
import fr.utln.logic.firstorder.Formula;
import fr.utln.logic.firstorder.io.FormulaReader;
import fr.utln.logic.firstorder.io.latex.LatexReader;
import fr.utln.logic.firstorder.io.unicode.UnicodeReader;
/**
* A third example of first-order logic API. This example focuses on formula I/O.
* @author Julien Seinturier - Université de Toulon / LIS umr CNRS 7020 - <a href="http://web.seinturier.fr">http://web.seinturier.fr</a>
*/
public class Example04 {
/**
* The main method
* @param args the main method arguments
*/
public static void main(String[] args){
FormulaReader reader = null;
// LaTeX
String latex = "\\forall{}e (P(e) \\to \\exists d ( P(d) \\wedge \\forall x ( I(m(x, y), d) \\implies I(m(f(x), f(y)), e))))";
reader = new LatexReader();
Formula fl = reader.read(latex);
System.out.println("LaTeX");
System.out.println(" Text : \""+latex+"\"");
System.out.println(" formula: " + fl);
System.out.println(" strict : " + fl.toStringStrict());
System.out.println();
// Unicode
String unicode = "\u2200e (P(e) \u2192 \u2203 d ( P(d) \u2227 \u2200 x ( I(m(x, y), d) \u2192 I(m(f(x), f(y)), e))))";
// Equivalent writing
//String unicode = "∀e (P(e) → ∃ d ( P(d) ∧ ∀ x ( I(m(x, y), d) → I(m(f(x), f(y)), e))))";
reader = new UnicodeReader();
Formula fu = reader.read(unicode);
System.out.println("Unicode");
System.out.println(" Text : \""+unicode+"\"");
System.out.println(" formula: " + fu);
System.out.println(" strict : " + fu.toStringStrict());
System.out.println();
// Loop
Formula fr = reader.read(fl.toStringStrict());
System.out.println("Loop");
System.out.println(" Text : \""+fl.toStringStrict()+"\"");
System.out.println(" formula: " + fr);
System.out.println(" strict : " + fr.toStringStrict());
}
}
Exécuter Example04 et s'assurer que les formules LaTeX sont bien lues.
|
4. Optimisations et fonctionnalités
Il est maintenant temps d'optimiser le package fr.utln.logic.firstorder et d'y ajouter quelques fonctionnalités.
4.1. Affichage
En utilisant les différents exemples, il apparait que les deux façon d'exprimer une formule de façon textuelle via les implantations toString() et toStringStrict() ne sont pas optimales. L'implantation actuelle de toString() peut provoquer un affichage erroné de formules alors que toStringStrict(), bien que valide, provoque un affichage peu lisible à cause des parenthèses.
| Exercice 30. |
|
Reprendre les implantations de la méthode toString() là ou cela est nécessaire afin d'avoir des affichages de formule valides tout en évitant une surcharge de parenthèses.
Afin de proposer un affichage compact, certaines règles peuvent être mises en place:
- Une formule sous la portée d'un quantificateur doit être entre parenthèse sauf si celle-ci débute également par un quantificateur
- Un connecteur suivi d'un connecteur de moindre priorité n'a pas besoin d'être entre parenthèse sauf si plusieurs occurrences de celui-ci se suivent
- Une formule a laquelle est appliquée une négation est entourée de parenthèses sauf si celle-ci débute par un quantificateur ou une négation
- Si une formule est une suite d'implications, celles-ci doivent être parenthésées de droite à gauche
Voici quelques exemples:
| Formule stricte |
Affichage |
| \( (((a\wedge{}b)\wedge{}c)\vee{}d) \) |
(a ∧ b ∧ c) ∨ d |
| \( (((a\vee{}b)\vee{}c)\wedge{}d) \) |
(a ∨ b ∨ c) ∧ d |
| \( \forall{}a{}\ (\exists{}b\ (P(a)\vee{}Q(b))) \) |
∀a ∃b (P(a) ∨ Q(b)) |
| \( \forall{}a{}\ (\exists{}b\ (P(a))) \) |
∀a ∃b P(a) |
| \( \forall{}a\ \exists{}b\ (\neg{}(P(a)\ \to\ P(b))\ \leftrightarrow\ ((a\wedge{}b)\wedge{}c)) \) |
∀a ∃b ¬(P(a) → P(b)) ↔ (a ∧ b ∧ c) |
Pour tester les implantations, ajouter dans le package fr.utln.logic.firstorder.example la classe Example05 dont le code est:
package fr.utln.logic.firstorder.example;
import fr.utln.logic.firstorder.Formula;
import fr.utln.logic.firstorder.io.FormulaReader;
import fr.utln.logic.firstorder.io.latex.LatexReader;
import fr.utln.logic.firstorder.io.unicode.UnicodeReader;
/**
* A fifth example of first-order logic API. This example focuses on formula display.
*/
public class Example05 {
/**
* The main method
* @param args the main method arguments
*/
public static void main(String[] args){
FormulaReader reader = new UnicodeReader();
String[] strs = new String[]{"(((a ∧ b) ∧ c) ∨ d)",
"(((a ∨ b) ∨ c) ∧ d)",
"∀a (∃b (P(a) ∨ Q(b)))",
"∀a (∃b (P(a)))",
"∀a (∃b (¬(P(a) → P(b))) ↔ ((a ∧ b) ∧ c))"};
Formula f = null;
for(String str : strs){
f = reader.read(str);
System.out.println("Input : "+str);
System.out.println("toString() : "+f);
System.out.println();
}
}
}
Et vérifier si les affichages produits correspondent bien au tableau.
|
4.2. Formule équivalentes
Au vu de la définition d'une formule de la logique du premier-ordre et des propriétés des connecteurs et des quantificateurs, il est possible d'exprimer une équivalence entre les formules. Par exemple la formule:
$$\forall{}x\ \forall{}y\ ( P(x)\wedge{}P(y)$$
est syntaxiquement equivalente à:
$$\forall{}y\ \forall{}x\ ( P(x)\wedge{}P(y)$$
ou encore à:
$$\forall{}y\ \forall{}x\ ( P(y)\wedge{}P(x)$$
On peut noter en effet que:
- L'ordre d'apparition des variables pour une suite du même quantificateur n'est pas significatif
- Les connecteurs \( \wedge{} \) et \( \vee{} \) sont commutatifs
| Exercice 31. |
|
Ajouter à l'interface Formula une methode equals(Formula f) qui renvoie true si la formule courante est syntaxiquement équivalente à f et false sinon. Implanter cette méthode partout là où cela est nécessaire.
|
... A venir: Tri lexicographique (Bonus)