Inférence à chaînage avant avec variables
=========================================

Le but de cette série est de développer un moteur d'inférence à chaînage avant capable de manipuler des règles comportant des variables. Dans un premier temps, vous devrez étendre l'implémentation des règles en complétant quelques fonctions utilitaires. Puis vous construirez un filtre, qui permettra de comparer deux propositions dont l'une pourra contenir des variables. Ensuite, en utilisant votre filtre, vous implémenterez un moteur d'inférence avec variables. 

Vous aurez également la possibilité d'implémenter un unificateur et de le tester sur votre moteur à chaînage avant avec variables. Un unificateur permet aussi de comparer deux propositions. La différence fondamentale avec le filtre est que l'unificateur accepte la présence de variables dans les deux expressions, ce qui rend possible de l'utiliser dans le chaînage arrière.

Modules squelettes
------------------

Les modules qui suivent constituent le squelette du programme que nous allons développer. Le dernier, ``exemple_impots_avec_variables.py``, est un module de test.

:download:`inference_avec_variables.zip <../../squelettes/inference_avec_variables.zip>`

.. Module ``.../moteur_avec_variables/__init__.py`` :

.. .. include::  ../../squelettes/inference_avec_variables/moteur_avec_variables/__init__.py
    :code:

Module ``.../moteur_avec_variables/proposition_avec_variables.py`` :

.. include::  ../../squelettes/inference_avec_variables/moteur_avec_variables/proposition_avec_variables.py
    :code:

Module ``.../moteur_avec_variables/regle_avec_variables.py`` :

.. include::  ../../squelettes/inference_avec_variables/moteur_avec_variables/regle_avec_variables.py
    :code:

Module ``.../moteur_avec_variables/chainage_avant_avec_variables.py`` :

.. include::  ../../squelettes/inference_avec_variables/moteur_avec_variables/chainage_avant_avec_variables.py
    :code:

Module ``.../moteur_avec_variables/filtre.py`` :

.. include::  ../../squelettes/inference_avec_variables/moteur_avec_variables/filtre.py
    :code:

Module ``.../moteur_avec_variables/unificateur.py`` :

.. include::  ../../squelettes/inference_avec_variables/moteur_avec_variables/unificateur.py
    :code:

Module ``.../exemple_impots_avec_variables.py`` :

.. include::  ../../squelettes/inference_avec_variables/exemple_impots_avec_variables.py
    :code:

Le code de cette série d'exercices s'appuie sur le code développé pour l'inférence sans variables. Il est donc important de respecter strictement la structure des dossiers que nous vous fournissons. Sinon, Python ne pourra pas importer correctement les modules.

Les faits et les règles
-----------------------

Au cours des exercices précédents, vous avez manipulé des faits simples et des règles sans variables. Dans cette série, les faits pourront être composées et les règles pourront contenir des variables. Nous parlerons plus généralement de *propositions* qui sont définies récursivement comme étant :

*  Un atome, présenté sous la forme d'une ``string`` et représentant soit une variable, soit une valeur ;
*  Ou un ``tuple`` contenant des propositions.

Vous trouverez `ici <https://docs.python.org/3.5/tutorial/datastructures.html#tuples-and-sequences>`__  des informations détaillées sur la syntaxe des ``tuples`` en Python. L'essentiel à retenir pour cet exercice est qu'un ``tuple`` est une séquence de valeurs construite en alignant plusieurs éléments séparés par des virgules : ``t3 = 'str', 0, []``. Pour plus de clarté, on entoure généralement ces valeurs de parenthèses : ``t3 = ('str', 0, [])``. Notez enfin qu'un tuple composé d'un seul élément doit contenir une virgule finale. Ici aussi, il est préférable d'utiliser des parenthèses : ``t1 = ('unique',)``.

Par convention, une variable sera un atome qui commence par un point d'interrogation. Exemple :

::

    '?x'
    '?qui'

Les faits seront des propositions sans variables. Voici par exemple deux descriptions de faits :

::

    'Paul'
    ('réduc-loyer', '200', 'Michel')

En général cependant, une proposition pourra contenir des variables, comme dans ces exemples :

::

    ('réduc-loyer', '200', '?x')
    ('bas-salaire', '?z')
    ('réduc-trajet', '?x', '?y')

Comme dans la série précédente, les règles seront constituées d'une liste de conditions et d'une conclusion. Voici les descriptions de deux règles possibles :

::

    [('pas-d-enfants', '?x')], ('réduc-enfant', '0','?x')
    [[('bas-salaire', '?x'), ('loyer', '?x')], ('réduc-loyer', '200', '?x')

La classe ``RegleSansVariables`` de la série précédente doit ainsi être adaptée à l'utilisation de variables. Nous créerons donc une nouvelle classe ``RegleAvecVariables`` qui devra redéfinir les méthodes ``depend_de`` et ``satisfaite_par`` en faisant appel aux méthodes de pattern matching (filtrage ou unification). Vous devrez compléter ces méthodes une fois le pattern matching implémenté.

Exercice 1 : Le filtre
----------------------

La technique du filtrage permet d'établir des correspondances entre deux propositions. Plus précisément, un filtre détermine les substitutions variables-valeurs qui permettent de retrouver une proposition sans variables (le datum) à partir d'une autre (le pattern) qui peut contenir des variables.

Pour commencer, vous devrez coder deux fonctions utilitaires dans le module ``proposition_avec_variables.py``, afin de faciliter l'implémentation du filtrage : La fonction ``est_atomique(prop)``, qui doit retourner ``True`` si la proposition ``prop`` est une ``string`` et la fonction ``est_une_variable(prop)``, qui doit retourner ``True`` si ``prop`` est un atome et si le premier caractère de sa description indique une variable (``'?'``).

La méthode ``Filtre.substitue``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Vous pouvez maintenant vous attaquer à la classe ``Filtre`` du module ``filtre.py`` et implémenter sa méthode ``substitue``. Cette méthode doit retourner un pattern dont les variables auront été remplacées par les valeurs disponibles dans l'environnement ``env`` qui est passé en paramètre. ``env`` est un dictionnaire qui contient des substitutions variable-valeur. N'oubliez pas que le pattern est une proposition, donc soit un atome, soit un tuple pouvant contenir d'autres propositions. Pensez donc à utiliser une méthode récursive pour traiter ces cas. 

Pour vous guider, voici quelques exemples du fonctionnement de la méthode :

::

    substitue('doctorant', {'?z': 'Paolo', '?y': 'Michel', '?x': 'Vincent'})
        -> 'doctorant'

    substitue('?x', {'?z': 'Paolo', '?y': 'Michel', '?x': 'Vincent'})
        -> 'Vincent'

    substitue(('?x', 'est un', 'doctorant'), {}) 
        -> ('?x', 'est un', 'doctorant')

    substitue(('?x', 'est un', 'doctorant'), {'?x': 'Vincent'}) 
        -> ('Vincent', 'est un', 'doctorant')

    substitue(('?x', 'est un', 'doctorant'), {'?y':'Michel'})
        -> ('?x', 'est un', 'doctorant')

    substitue(('?x', 'est un', ('?a')), {'?y': 'Michel', '?a': 'Vincent'})
        -> ('?x', 'est un', ('Vincent'))

La méthode ``Filtre.filtre``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Nous allons maintenant écrire la méthode ``filtre`` de la classe ``Filtre``, qui implémente l'algorithme de filtrage. La méthode retournera :

*  Un environnement, ``{'?x':'toto', ..., '?y':'titi'}``, dans le cas où le processus aboutit à des substitutions ;
*  Un environnement vide, ``{}``, si le processus réussit sans aucune substitution, c'est-à-dire lorsque les deux propositions sont identiques ;
*  La constante ``Filtre.echec``, en cas d'erreur de filtrage (si ``datum`` et ``pattern`` sont incompatibles).

La méthode possède la signature ``filtre(datum, pattern)``, où ``datum`` est une proposition sans variables, et ``pattern`` une proposition pouvant contenir des variables. 

Voici son pseudo-code :

::

    Filtre(datum, pattern)
    1.  IF pattern == () AND datum == () THEN RETURN {}
    2.  ELSE IF pattern == () OR datum == () THEN RETURN échec
    3.  ELSE IF pattern est un atome THEN
    4.      IF pattern et datum sont identiques THEN RETURN {}
    5.      ELSE IF pattern est une variable THEN RETURN {pattern: datum}
    6.      ELSE RETURN échec
    7.      END IF
    8.  ELSE IF datum est un atome THEN RETURN échec
    9.  ELSE 
    10.     F1 <- premier (datum)
    11.     T1 <- reste (datum)
    12.     F2 <- premier(pattern)
    13.     T2 <- reste(pattern)
    14.     Z1 <- Filtre(F1, F2)
    15.     IF Z1 == échec THEN RETURN échec END IF	
    16.     G1 <- T1
    17.     G2 <- remplacer les variables de T2 selon les substitutions de Z1
    18.     Z2 <- Filtre(G1, G2)
    19.     IF Z2 == échec THEN RETURN échec END IF		
    20.     RETURN {Z1 UNION Z2}
    21. END IF
    END Filtre

Et voici quelques exemples de son usage :

::

    filtre('Vincent', '?x')
        -> {'?x': 'Vincent'}

    filtre(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', 'doctorant'))
        -> {'?x': 'Vincent'}

    filtre(('Vincent', 'est un', 'doctorant') , ('Vincent', 'est un', 'doctorant'))
        -> {}

    filtre(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', '?x'))
        -> échec

    filtre(('Vincent', 'est un', ('doctorant')) , ('Vincent', 'est un', ('?y')))
        -> {'?y': 'doctorant'}

Vous trouverez `ici <http://docs.python.org/py3k/library/stdtypes.html#typesmapping>`__  quelques fonctions utiles pour manipuler des dictionnaires. La méthode ``update`` est particulièrement commode pour obtenir l'union de deux dictionnaires. Une façon brève et élégante pour retourner un dictionnaire à un élément est : ``return {key: value}``.

La vraie fonction d'interface : ``Filtre.pattern_match``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

La fonction ``filtre`` n'est pas très pratique pour un programme hôte car il n'est pas possible de lui fournir en entrée un environnement. Dans le processus de chaînage avant que vous allez écrire, chaque condition d'une règle doit être vérifiée avant de pouvoir être utilisée. Cela implique que chaque condition soit filtrée avec succès par un fait existant. Comme plusieurs conditions peuvent être présentes, il est nécessaire de tester chacune en respectant l'environnement obtenu lors des filtrages précédents. Il faut donc pouvoir fournir à la fonction de filtrage un environnement déjà établi.

Vous devez ainsi compléter la méthode ``pattern_match`` de la classe ``Filtre``, qui permettra de prendre en compte un environnement de substitutions déjà existantes. Cette méthode prend en paramètres deux propositions, un datum et un pattern, accompagnés d'un environnement sous forme d'argument optionnel, et retourne un nouvel environnement. Elle s'appuiera bien évidemment sur les méthodes ``filtre`` et ``substitue``.

Voici une liste d'exemples qui prennent en compte des environnements préexistants :

::

    pattern_match(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', 'doctorant') , {'?y':'doctorant'})
        -> {'?y': 'doctorant', '?x': 'Vincent'}

    pattern_match(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', '?y'), {'?y':'doctorant'})
        -> {'?y': 'doctorant', '?x': 'Vincent'}`

    pattern_match('Vincent', '?x', {})
        -> {'?x': 'Vincent'}

Les arguments optionnels d'une fonction Python obéissent à la syntaxe ``argument=valeur``. À cause de problèmes de mutabilité, il est déconseillé d'utiliser ``{}`` comme valeur par défaut. Il est préférable d'utiliser la valeur ``None`` et d'assigner un dictionnaire vide à la variable à l'intérieur de la méthode. Vous pouvez consulter cet `article <http://www.deadlybloodyserious.com/2008/05/default-argument-blunders>`__  pour plus d'informations.

Exercice 2 : De retour aux règles
---------------------------------

La méthode ``RegleAvecVariables.depend_de``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

La classe ``RegleAvecVariables``, qui est à implémenter dans le module ``regle_avec_variables.py``, reprend les noms des méthodes de la classe ``RegleSansVariables`` que nous avons utilisée dans l'exercice précédent, mais avec une implémentation passablement différente. La méthode ``dépend_de`` doit ainsi vérifier qu'un fait passée en paramètre est un déclencheur des conditions de la règle, et doit retourner un dictionnaire associant à chaque condition de la règle l'environnement résultant du pattern match entre cette condition et le fait. Si la recherche de substitutions aboutit à un échec pour une condition, il n'est pas nécessaire de la mentionner dans le dictionnaire. Il faudra donc comparer chaque condition de la règle avec le fait à l'aide de la méthode ``pattern_match`` du filtre, et recueillir les environnements résultants.

La méthode ``dépend_de`` prend en entrée deux paramètres : ``fait``, un fait à tester, et ``methode``, l'objet de pattern matching utilisé, soit un filtre soit un unificateur (filtre par défaut). N'oubliez pas que cette fonction doit vérifier toutes les conditions de la règle. Pour vous aider, voici quelques exemples :

::

    règle = RegleAvecVariables([('père', '?x', '?y'), ('père', '?y', '?z')], 
                                ('grand-père, '?x', '?z'))

    # Le fait ('père', 'Jean', 'Paul') peut satisfaire la première ou la seconde condition :
    methode.pattern_match(('père', 'Jean', 'Paul'), ('père', '?x', '?y'))
        -> {'?x': 'Jean', '?y': 'Paul'}

    methode.pattern_match(('père', 'Jean', 'Paul'), ('père', '?y', '?z'))
        -> {'?y': 'Jean', '?z': 'Paul'}

    # La méthode depend_de renvoie donc un dictionnaire avec ces deux conditions associées
    # à leurs environnements respectives :
    depend_de(('père', 'Jean', 'Paul'), Filtre())
        -> {('père', '?x', '?y'): {'?x': 'Jean', '?y': 'Paul'}, 
            ('père', '?y', '?z'): {'?y': 'Jean', '?z': 'Paul'}}

  

La méthode ``RegleAvecVariables.satisfaite_par``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

La méthode ``satisfaite_par`` de la classe ``RegleAvecVariables`` vérifie que les faits passés en paramètres satisfont toutes les conditions de la règle tout en respectant l'environnement de départ inféré par ``dépend_de``. La méthode prend en entrée quatre paramètres :

*  ``faits`` : une liste de faits ;
*  ``cond``: la condition qui a servi à découvrir l'environnement ``env`` ;
*  ``env`` : l'environnement déjà établi par ``depend_de`` ;
*  ``methode`` : l'objet de pattern matching utilisé, soit un filtre soit un unificateur (filtre par défaut).

La méthode ``satisfaite_par`` retourne une liste d'environnements qui correspondent à toutes les substitutions possibles entre les conditions de la règle et les faits. Il s'agit donc de trouver les environnements qui satisfont chacun toutes les conditions. Si ce n'est pas possible, la méthode doit retourner une liste vide. En outre, chaque nouvel environnement construit devra être testé lors de la vérification de la prochaine condition. Voici un petit exemple pour clarifier les choses :

::

    faits = [('père', 'Jean', 'Paul'), 
             ('père', 'Florent', 'Paul'),
             ('père', 'Paul', 'Michel')]

    regle = RegleAvecVariables([('père', '?x', '?y'), ('père', '?y', '?z')], 
                               ('grand-père, '?x', '?z'))

    satisfaite_par(faits, ('père', '?x', '?y'), {'?x': 'Jean', '?y': 'Paul'})
        -> [{'?x': 'Jean', '?y': 'Paul', '?z': 'Michel'}]

    satisfaite_par(faits, ('père', '?y', '?z'), {'?y': 'Jean', '?z': 'Paul'})
        -> []

Et voici le pseudo-code de la méthode :

::

    SatisfaitePar(regle, faits, cond, env)	
    1.  conditions_a_tester <- conditions de regle sauf cond
    2.  environnements_satisfaisants <- [env]
    3.  FOR EACH condition cond1 de conditions_a_tester DO
    4.      environnements_nouveaux <- liste vide
    5.      FOR EACH fait de faits DO
    6.          FOR EACH environnement env1 de environnements_satisfaisants DO
    7.              env1 <- nouvel environnement déterminé par pattern_match(fait, cond1, env1)
    8.              IF NOT env1 == échec THEN
    9.                  ajouter env1 à environnements_nouveaux
    10.             END IF
    11.         END FOR
    12.     END FOR
    13.     IF environnements_nouveaux est vide THEN
    14.         RETURN liste vide
    15.     END IF
    16.     environnements_satisfaisants <- environnements_nouveaux
    17. END FOR
    18. RETURN environnements_satisfaisants
    END SatisfaitePar

Exercice 3 : Le moteur d'inférence à chaînage avant avec variables
------------------------------------------------------------------

La méthode ``ChainageAvantAvecVariables.instancie_conclusion``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Une fois que les conditions d'une règle ont été validées, il faut instancier la conclusion en accord avec la liste des environnements ainsi obtenus afin de déduire de nouvelles propositions. La fonction ``instancie_conclusion`` de la classe ``ChainageAvantAvecVariables`` prend comme paramètres une règle et une liste d'environnements, et retourne une liste de nouveaux faits (un par environnement). Exemples :

::

    règle = RegleAvecVariables(liste_de_conditions, ('?x', '?y'))
    instancie_conclusion(règle, [{'?x': 'X','?y': 'Y'}])
        -> [('X', 'Y')]

Vous pouvez implémenter cette méthode de façon simple avec une boucle ``for`` itérant sur les environnements, mais il est aussi possible de l'écrire en une ligne sous forme de list `comprehension <https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions>`__  (un peu plus complexe mais plus élégant). Pensez en outre à utiliser la fonction ``substitue`` de la classe de pattern match.

La méthode ``ChainageAvantAvecVariables.chaine``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Grâce aux changements apportés aux règles, grâce à la méthode ``instancie_conclusion`` et au module de filtrage que nous avons développé, nous pouvons maintenant réaliser une nouvelle version de notre moteur d'inférence à chaînage avant, avec la capacité de manipuler des règles comportant des variables.

Implémentez l'algorithme de chaînage avant dans la méthode ``chaine`` de la classe ``ChainageAvantAvecVariables``. Pour rappel, l'algorithme à implémenter est le suivant :

::

    ChainageAvantAvecVariables(faits_depart, regles)
    1.  solutions <- liste vide
    2.  Q <- faits_depart	
    3.  WHILE Q n'est pas vide DO
    4.      q <- premier(Q)
    5.      Q <- reste(Q)
    6.      IF q n'est pas dans solutions THEN 
    7.          ajouter q à solutions
    8.          FOR EACH règle r de regles DO
    9.              IF une condition de r dépend de q THEN
    10.                 FOR EACH condition cond et environnement env déduits de r.depend_de(q) DO
    11.                     envs <- r.satisfaite_par(solutions, cond, env)
    12.                     instances <- instancier la conclusion de r selon envs
    13.                     ajouter instances en queue de Q 
    14.                 END FOR
    15.             END IF
    16.         END FOR 
    17.     END IF
    18. END WHILE
    19. RETURN solutions
    END ChainageAvantAvecVariables 

Test du programme : Chaînage avant avec filtre
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Le module ``exemple_impots_avec_variables.py`` contient les règles et les faits nécessaires pour le calcul du montant d'une réduction d'impôts. Après avoir écrit votre programme, testez-le en exécutant dans le terminal avec l'option ``filtre``. Vous pouvez afficher la trace en ajoutant l'option ``trace``. Quelle devrait être la réduction d'impôts ? 

::

    python3 exemple_impots_avec_variables.py filtre
    python3 exemple_impots_avec_variables.py filtre trace

Exercice 4 : L'unificateur [OPTIONNEL]
--------------------------------------

Nous allons maintenant construire un unificateur. C'est un outil analogue à un filtre mais bien plus puissant. À la différence du filtre, qui compare deux expressions dont l'une seulement peut comporter des variables, l'objectif d'un unificateur est de comparer deux expressions pouvant toutes deux contenir des variables. L'unificateur est donc une version généralisée du filtre. Il fournit comme résultat les correspondances entre les deux propositions sous la forme de substitutions variable-proposition (lorsqu'il en existe).

Pour mieux comprendre l'utilité de l'unificateur, voyons quelques exemples :

::

    unifie(('Vincent', 'est un', 'doctorant'), ('Vincent', 'est un', 'doctorant'))
       -> {}

    unifie(('Vincent', 'est un', 'doctorant'), ('Michel', 'est un', 'doctorant'))
       -> échec

    unifie(('foo', '?x', ('?y', 'bar', 'Jean')), ('foo', 'Jean', ('Marc', 'bar', '?x')))
       -> {'?y': 'Marc', '?x': 'Jean'}

    unifie(('p', '?x', ('f', '?y')), ('p', ('f','a'), '?x'))
       -> {'?y': 'a', '?x': ('f', 'a')}`

Nous utiliserons des conventions analogues à celles que nous avons appliquées dans le cas du filtrage. Désormais cependant un environnement pourra associer des **propositions, pas nécessairement des valeurs,** à des variables. Le résultat final de l'unification contiendra plutôt des substitutions variable-proposition sans variables, mais les étapes intermédiaires pourront aussi renvoyer des environnements qui associent des propositions contenant des variables à d'autres variables. 

La méthode ``Unificateur.substitue``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

La fonction ``substitue`` de la classe ``Unificateur`` doit permettre d'instancier une proposition étant donné un ensemble de substitutions variable-proposition. Exemples :

::

    substitue(('?x', 'est un', 'doctorant') , {})
      -> ('?x', 'est un', 'doctorant')

    substitue(('p', '?x') , {'?y': ('g','?z'), '?x': ('f', '?y'), '?z': ('a')})
      -> ('p', ('f', ('g', ('a'))))

    substitue(('p', '?x'), {'?y': ('g','?z'), '?x': ('f','?y'), '?z': ('?q')})
       -> ('p', ('f', ('g', ('?q'))))

Lorsque vous rédigerez le code de cette méthodes, rappelez-vous qu'une variable doit parfois être remplacée par une définition de substitution qui contient elle-même des variables. Il faut donc veiller à aussi remplacer toutes les variables qui figurent dans la proposition associée à une autre variable. Pensez à implémenter une solution récursive. 

La méthode ``Unificateur.unifie``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Vous avez maintenant tout ce qu'il faut pour implémenter la fonction ``Unificateur.unifie``. L'algorithme d'unification est très proche de l'algorithme de filtrage :

::

    Unifie(prop1, prop2)
    1.  IF prop1 ou prop2 est un atome THEN
    2.      si nécessaire, échanger prop1 et prop2 pour que prop1 soit un atome
    3.      IF prop1 et prop2 sont identiques THEN RETURN {}
    4.      ELSE IF prop1 est une variable THEN
    5.          IF prop1 apparaît dans prop2 THEN RETURN échec
    6.          ELSE RETURN {prop1: prop2}
    7.          END IF 
    8.      ELSE IF prop2 est une variable THEN RETURN {prop2: prop1}
    9.      ELSE RETURN échec
    10.     END IF
    11. ELSE
    12.     F1 <- premier(prop1)
    13.     T1 <- reste(prop1)
    14.     F2 <- premier(prop2)
    15.     T2 <- reste(prop2)
    16.     Z1 <- Unifie(F1, F2)
    17.     IF Z1 == échec THEN RETURN échec END IF
    18.     G1 <- remplacer les variables de T1 selon les substitutions de Z1
    19.     G2 <- remplacer les variables de T2 selon les substitutions de Z1
    20.     Z2 <- Unifie(G1, G2)
    21.     IF Z2 == échec THEN RETURN échec END IF
    22.     RETURN {Z1 UNION Z2}
    23. END IF
    END Unifie

La vraie fonction d'interface : ``Unificateur.pattern_match``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Comme dans le cas de la méthode ``filtre``, ``unifie`` n'est pas très pratique pour un programme hôte. Il faut donc aussi coder une fonction ``Unificateur.pattern_match`` qui permette de prendre en compte un environnement de substitutions déjà existantes. Cette fonction prendra en paramètres deux expressions pouvant contenir des variables et un environnement à titre d'argument optionnel. Elle doit retourner un nouvel environnement ou la constante ``Unificateur.echec``, selon que le pattern matching a réussi ou échoué. La méthode s'appuiera bien évidemment sur ``unifie`` et ``substitue``.

Voici une liste d'exemples qui prennent en compte des environnements préexistants :

::

    pattern_match(('foo', '?x', ('?y', 'bar', 'Jean')), ('foo', 'Jean', ('Marc', 'bar', '?x')), {'?y': 'Marc'})
       -> {'?y': 'Marc', '?x': 'Jean'}

    pattern_match(('foo', '?x', ('?y', 'bar', 'Paul')), ('foo', 'Jean', ('Marc', 'bar', '?x')), {})
       -> échec

Pour coder cette méthode, il convient donc de s'assurer que l'environnement est valide, puis de remplacer les variables des deux propositions par les définitions de l'environnement, et enfin de procéder à l'unification.

Test du programme : Chaînage avant avec unificateur
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Essayez d'utiliser l'unificateur à la place du filtre, en lançant ``exemple_impots_avec_variables.py`` avec l'option `unificateur'. Que constatez-vous et pourquoi ?

::

    python3 exemple_impots_avec_variables.py unificateur
    python3 exemple_impots_avec_variables.py unificateur trace

Est-il vraiment nécessaire d'utiliser un unificateur dans le chaînage avant ?
