Cet article est dédié à la mémoire de John Vlissides (1961-2005).



A qui s'adresse cet article?

Cet article s'adresse :

  • aux débutants, voulant apprendre rapidement certaines techniques de programmation en Prolog
  • aux personnes plus expérimentées, souhaitant découvrir de nouvelles techniques et une nouvelle approche de la programmation en Prolog

Introduction

Aux alentours de 1994, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides publient le livre Design Patterns: Elements of Reusable Object-Oriented Software (aussi connu sous le nom de Gang of Four book). Ce livre fut à l'origine d'une petite révolution dans le domaine de la programmation orientée-objets puisqu'il présentait une manière de concevoir des logiciels au moyen de "Design Patterns" (motifs de conception): des diagrammes de classes que l'on peut combiner entre eux de manière à obtenir une application cohérente.

L'idée fondammentale derrière les Design Patterns est la réutilisation des techniques de conception ayant fait leurs preuves. Ainsi, chaque Pattern est accompagné d'une description, d'une discussion sur les cas favorables à son utilisation, de plusieurs exemples d'implémentation, etc.

C'est ce principe qui est repris ici, mais adapté à la programmation Prolog. Le but de donner aux programmeurs Prolog des idées sur la manière d'écrire un programme Prolog (et de concevoir les différents prédicats).

Les Designs Patterns du GoF (Gang of Four) étaient au départ au nombre de 23. Depuis, de nombreux autres motifs sont apparus dans le domaine de la programmation Orientée-Objets. Les Design Patterns ont également donné naissance à d'autres "langages de motifs" (Pattern Languages) dans des domaines différents (dont celui-ci, sur la programmation en Prolog). Historiquement, les Design Patterns eux-mêmes ont été inspirés par les travaux de Christopher Alexander dans le domaine de l'architecture...



Les motifs de programmation Prolog sont (pour l'instant) au nombre de 4. Pourquoi si peu de motifs, vous demandez-vous? Parce que nous ne présentons que des motifs réellement utilisés (et réutilisables) au sein de projets Prolog. Nous ne "fabriquons" pas de motifs pour "faire du remplissage" (ou faire parler de nous). Ce n'est qu'avec le temps et au travers de divers projets que de nouveaux motifs peuvent émerger.

Ces motifs sont classés comme suit:

  • 3 motifs "de base", permettant d'écrire quasiment n'importe quel programme Prolog
  • 1 autre motif, facilitant l'écriture de certains prédicats



Puisque la mode en informatique, c'est de donner des noms rigolos à tout ce que l'on invente, j'ai décidé de nommer ces motifs les PPPP ou 4P (prononcez "four Pea", pour faire plus classe) : Pcaboche Prolog Programming Patterns.

Comme disait Donald Knuth :

"The most important thing in the programming language is the name. A language will not succeed without a good name. I have recently invented a very good name and now I am looking for a suitable language."

:-)

1. Motifs de base

La programmation en Prolog peut s'articuler autour de 3 Patterns de base, décrits dans cette section.

Ces patterns ont pour but de répondre à plusieurs questions:

  • comment écrire un programme même sans connaissance approfondie du langage Prolog ?
  • comment structurer un programme Prolog ?
  • comment utiliser le symbole "!" ? (et comprendre au passage à quoi il sert)
  • en résumé: apprendre à programmer en Prolog

La plupart des programmes Prolog sont une combinaison des 3 Patterns de base (sauf cas très particuliers).


Pour comprendre les deux premiers Patterns, nous allons chercher à écrire un prédicat qui génère une liste de longueur N remplie de 0.

Pattern 1 : Elimination des cas d'erreur

Le premier pattern nous permet de dire « en cas de paramètres incorrects, le prédicat échoue ».

Il se présente sous cette forme:

 
Sélectionnez
predicat(parametres) :-
  conditon_echec,
  !, fail.

Dans notre cas, la liste générée ne peut pas être de longueur négative:

Exemple
Sélectionnez
genereListeZeros(N, _) :-
  N<0,
  !, fail.


Ce pattern apparaît donc toujours en premier. Répétez ce pattern autant de fois que nécessaire, pour chaque cas d'erreur.

Pattern 2 : Exécution dirigée

Ce pattern va nous permettre d'exprimer des expressions du genre :

 
Sélectionnez
si condition_1 alors
  action_1
sinon si condition_2 alors
  action_2
sinon
  ...
fin_si

Et voici sa structure:

 
Sélectionnez
predicat(parametres) :-
  conditon_1, !,
  action_1.

predicat(parametres) :-
  conditon_2, !,
  action_2.

...



Dans notre exemple :

Exemple
Sélectionnez
genereListeZeros(N, []) :- 
  N==0,
  !.

genereListeZeros(N, [0|Q]) :- 
  !,
  N1 is N-1,
  genereListeZeros(N1, Q).



Avec ce pattern, on dirige l'exécution dans une et une seule direction (pas d'énumération de plusieurs solutions), c'est pourquoi je l'ai appelé « exécution dirigée ».

On remarque ici que, comme pour tout autre langage fonctionnel, les conditions d'arrêt sont placées en premier (ici, on s'arrête dès que N=0).



Astuce d'optimisation:
Lorsque vous utilisez ce pattern, pensez à mettre des cuts ( ! ) dans toutes le clauses. En effet, lorsque vous utilisez un cut, vous ne laissez pas de point de choix, donc Prolog n'est pas obligé de garder en mémoire les informations de retour-arrière, donc le programme utilise moins de mémoire !



Au final, à l'aide des patterns 1 et 2, on obtient le prédicat suivant:

Programme final
Sélectionnez
% Elimination des cas d'erreur
genereListeZeros(N, _) :-     % Cas N<0
  N<0,  !, fail. 

% Execution dirigée
genereListeZeros(N, []) :-    % Cas N=0
  N==0,
  !.

genereListeZeros(N, [0|Q]) :-   % Les autres cas (N>0)
  !,
  N1 is N-1,
  genereListeZeros(N1, Q).



Pattern 3 : Enumération de solutions

Le troisième pattern est utile pour l'énumération des solutions.

Sa structure est la suivante:

 
Sélectionnez
predicat(Entrees, Sorties) :-
  ...
  Sorties = solution1.
  
predicat(Entrees, Sorties) :-
  ...
  Sorties = solution2.
  
...



Pour illustrer ce pattern, nous allons vous présenter une fonction qui énumère les combinaisons possibles entre un entier et une liste (sous forme de tuples) :

Exemple
Sélectionnez
enum(X, [T|_], (X,T) ).
enum(X, [_|Q], R) :- enum(X, Q, R).

Execution:

Execution
Sélectionnez
| ?- enum(1, [1,2,3,4], R).
  R = 1,1 ? ;
  R = 1,2 ? ;
  R = 1,3 ? ;
  R = 1,4 ? ;
  
No



Utilisation

Lorsque l'on rédige un prédicat, on commence toujours par éliminer les cas d'erreur, s'il y en a. Ensuite, on utilise soit le Pattern 2 soit le Pattern 3 selon que le prédicat doit renvoyer une ou plusieurs solution(s). Suivant ce principe, la syntaxe d'un prédicat Prolog devient :

predicat ::= pattern1* (pattern2+ | pattern3+)



Voici donc la grammaire correspondante (hautement simplifiée !) :

predicat ::= pattern1* (pattern2+ | pattern3+)
pattern1 ::=


nomPredicat ( parametres ) :-
   conditions_echec
   ,
!, fail.
pattern2 ::=



nomPredicat ( parametres ) :-
   conditions
   , !,
instructions .
pattern3 ::=

nomPredicat ( parametres ) :-
   instructions .
conditions_echec ::= condition+
conditions ::= condition*



Ces différents Patterns nous ont par ailleurs permis d'apprendre la bonne utilisation du Cut (!) et du fail.




2. Autre motif

Pattern 4: Génération de listes

Applicabilité :

Ecrire un prédicat qui retourne une liste d'éléments


Implémentation :

  1. Ecrire un prédicat qui retourne les éléments de la liste 1 par 1 (pattern 3)
  2. Ecrire un prédicat qui effectue un findall/3 sur le prédicat précédent



Exemple :

Le prédicat suivant (combinaisons/2) prend comme paramètre une liste de listes et retourne comme résultat la liste de toutes les combinaisons qu'il est possible d'obtenir en prenant un élément de chacune de ces listes.

 
Sélectionnez
%%
% Retourne les combinaisons une par une
%
combi([L|Q], [Elem|R]) :-
  member(Elem, L),
  combi(Q, R).

combi([], []).



%%
% Retourne la liste de toutes les combinaisons
%
combinaisons(Listes, Combis) :-
  findall(C, combi(Listes, C), Combis).
Exécution
Sélectionnez
|?- combinaisons([[a,b], [c,d,e], [f]], Combis).

Combis = [[a, c, f], [a, d, f], [a, e, f], [b, c, f], [b, d, f], [b, e, f]] 



Le pattern 4 est très intéressant car il se base sur le pattern 3, ce qui lui confère une grande modularité.



Grammaire :

pattern4 ::=



pattern3+

predicat_liste(Param, L) :-
  findall(X, predicat(Param, X), L).



Avantages :

  • Prédicats très simples à écrire
  • Solution très simple à comprendre et à maintenir
  • Très modulable (les prédicats sont utilisables indépendemment l'un de l'autre, suivant le contexte)

Inconvénient :

  • Légèrement moins rapide qu'une solution sans findall/3 (ne devient un problème que si le prédicat utilisé dans le findall fait un usage intensif de la pile)



Conclusion :

Cette implémentation se révèle extrêmement pratique dans de nombreux cas portant sur les listes et simplifie l'implémentation, la compréhension et la maintenance.