Вы находитесь на странице: 1из 62

Programmation

fonctionnelle, impérative, logique et orientée objet
Introduction
● Les langages de programmation permettent de décrire 
des calculs de façon plus abstraite qu'un programme 
“machine”.
● Les programmes dans ces langages sont
– Compilés (transformés en “code machine” par un compilateur)
– Ou interprétés (exécutés par un autre programme appelé interpréteur)
● Un compilateur génère un programme machine pour un 
processeur donné, dont la sémantique est donnée par le 
programme source.
● Un interpréteur lit un programme source et « calcule » 
sa sémantique directement pour une entrée donnée
Compilation
Compilateur Programme
(C, Caml,  Programme
(gcc, ocamlopt) machine
Fortran...)
Processeur
Le programme 
compilé est  Programme
Entrées Sorties
machine
très efficace
Processeur

Interprétation
Programme
(Lisp)
+ Interpréteur Sorties
entrées
Processeur
peu efficace
Exemple de compilation : factorielle
Bytecode
Java
   0:   iconst_1
   1:   istore_2
   2:   iconst_1
int k = 1, l = 1;    3:   istore_3
while (k <= i) {    4:   iload_2
   5:   iload_0
  l = l * k;    6:   if_icmpgt       19
  k++;    9:   iload_3
   10:  iload_2
}    11:  imul
   12:  istore_3
return l;
   13:  iinc    2, 1
   16:  goto    4
   19:  iload_3
   20:  ireturn
Compilation Programme
Programme Compilateur
pour une “bytecode”
machine Processeur
virtuelle
(Java, Caml, 
.Net)
Programme
Machine
“bytecode” Sorties
virtuelle X
+ entrées
Le programme 
Processeur X
est portable et 
moyennement   Programme
Machine
efficace “bytecode”
virtuelle Y
Sorties
+ entrées
Processeur Y
Machine virtuelle

● Une machine virtuelle est un programme (compilé) qui 
exécute des programmes (appelés bytecode). En ce sens, 
c'est un interpréteur.
● Le langage d'entrée est proche d'un langage machine 
(d'où le nom de machine virtuelle).
● Les programmes à exécuter sont obtenus par compilation 
à partir d'un langage de haut niveau.
● Il y a autant de versions de la machine virtuelle 
(compilée) que de machines réelles différentes.
Conclusion (de l'introduction)
● Les compilateurs, interpréteurs, machines virtuelles se 
chargent :
– De l'interface avec une machine simple (processeur réel)
– des performances (en partie)
– de la portabilité (exécution sur différentes machines)
● Les langages de programmation offrent :
– une meilleure expressivité (moyens de structurer les 
programmes, de raisonner de façon plus abstraite)
– une spécialisation plus ou moins grande pour des types de 
problèmes (cas extrême : les langages dédiés)
– certaines garanties de correction, de sécurité, grâce à des 
concepts sémantiques forts (typage...)
Plan du cours
● Programmation fonctionnelle
– Exemples, ordre d'évaluation, exécution par interprétation, 
● Programmation impérative
– Exemples, portée des identificateurs, gestion de la mémoire, 
compilation
● Typage et gestion de la mémoire
– typage statique (lambda­calcul simplement typé), ramasse­miettes
● Programmation orientée objet
– Exemples, modularité, héritage, liaison dynamique.
● Programmation logique
– Exemples, sémantique déclarative et opérationnelle
Portée des identificateurs
● La portée d'une définition (d'un identificateur) dans une 
expression est la partie de l'expression dans laquelle cette 
définition est active. En lambda­calcul, c'est une notion purement 
syntaxique (donc pas liée à l'exécution).
– Dans (x.e) e', x est lié à e' dans e, mais uniquement pour les 
occurrences libres de x dans e.
– De même pour let x = e' in e (les deux expressions sont d'ailleurs 
équivalentes)
● La portée est illustrée par la propriété suivante de la relation d'alpha­
équivalence :
–  si e ~ e' et e  e'' alors il existe e''' tel que e'' ~ e''' et e'  e'''
● Cela signifie qu'on peut voir considérer les lambda­termes “à alpha­
équivalence près”.
Extensions du lambda­calcul
● On peut enrichir le lambda­calcul avec
– Des entiers et des opérateurs : 0, 1, ..., +, en étendant la bêta­
réduction : (+ 2) 3  5
– Des paires et des destructeurs : (., .), fst, snd
– Des constructeurs, pour représenter les “types sommes”
– Des définitions de constantes locales : let x = e in e
– La condition : if e then e else e
– La récursivité : let rec f x = ...f...
● Tous ces “ajouts” sont représentables en lambda­calcul pur 
(cependant pour la récursivité, la terminaison dépend de 
l'ordre de réduction)
Programmation fonctionnelle
● La programmation fonctionnelle pure se contente du lambda­
calcul plus ou moins étendu (exemple : le langage Haskell).
● Les lambda­termes clos représentent des fonctions au sens 
mathématique. Le résultat ne change pas d'un appel à l'autre, 
il ne dépend pas d'un quelconque « état » ou « contexte » 
autre que les arguments.
● Propriété de transparence référentielle :
On peut toujours remplacer une expression par sa valeur.
– En particulier, une application de fonction peut être remplacée 
par son résultat, ou (par transitivité) par l'appel d'une autre 
fonction « équivalente » à la première. Intuitivement, seul le 
résultat compte (pas d'effets de bord).
● Les fonctions sont des valeurs comme les autres : au cours de 
l'exécution on peut les créer, les appliquer, les retourner en 
résultat, les passer en argument d'autres fonctions.

● Les données ont le plus souvent une structure récursive 
(listes, arbres) ainsi que les fonctions qui les manipulent.
Bêta­réduction et ordre de réduction
● Bêta­réduction : réduction non déterministe
la substitution e[e ' / x ]est licite e e' e e' e e'
 x.e e' e [e' / x]  x.e  x.e ' e e' '  e' e' ' e ' ' e e' ' e'

● Caml : les arguments d'abord (non spécifié officiellement)

x.e est une  e e' e e'


 x.ee'  e[e ' / x] forme normale e e' '  e' e' ' e ' ' e e' ' e'

prioritaire par rapport 
aux deux autres règles

● Évaluation paresseuse (Haskell) :
x.e est une  e e'
 x.ee'  e[e ' / x] forme normale e e' '  e' e' '

remplacement 
sans recopie !
Ordre de réduction
● L'ordre d'évaluation n'a d'importance que pour :
– La terminaison (c'est au programmeur d'écrire des programmes qui 
terminent pour l'ordre considéré, mais cet ordre importe rarement)
– L'efficacité (certains chemins sont moins coûteux que d'autres)
– Les programmes impératifs (références, tableaux, entrées­sorties).
● Évaluation en caml : opérandes d'abord
– Toute lambda­abstraction est une valeur (ou forme normale) et n'est 
donc pas réduite (pensez aux effets de bord, ex : fun x ­> y := 0)
– Pour réduire (e e'), on réduit e' jusqu'à obtenir une lambda­
abstraction, puis on réduit e, puis l'expression globale.
Cette réduction est plus faible que la bêta­réduction, (on ne réduit pas 
“sous les lambda”) mais plus naturelle s'il y a des effets de bord. 
● Évaluation paresseuse (en Haskell par exemple) : on ne réduit que 
les expressions indispensables, et au maximum une fois.
● En caml :
(x.y.(+ x x)) (+ 1 1) (+ 2 2) 
(x.y.(+ x x)) (+ 1 1) 4 
(x.y.(+ x x)) 2 4 
(y.(+ 2 2)) 4 
+ 2 2 
4

● Évaluation paresseuse : (appel par nom)
(x.y.(+ x x)) (+ 1 1) (+ 2 2) 
(y.+ ((+ 1 1) (+ 1 1))) (+ 2 2)  on ne duplique pas réellement + 1 1
+ (+ 1 1) (+ 1 1)  + 2 2 n'est jamais évalué
+ 2 2 

Un interpréteur simplissime
● Cette fonction évalue un terme clos (c'est à dire sans variable libre).
let rec eval = function
| e e'  let e'r = eval e' in
             let x.e'' = eval e in (* Sinon, on est bloqué *)
             eval e''[e'r / x]
| e  e (* c'est une valeur *)
● Pour éviter de faire des remplacements coûteux, on préfère évaluer un 
terme dans un contexte, qui associe des termes à des identificateurs :
let rec eval c = function
| e e'  let e'r = eval c e' in
             let x.e'' = eval c e in
             eval [e'r / x] :: c e''
| x  c(x)
| e  e
où [e'r / x] :: c est le contexte c enrichi par l'association de x  e'r. Tout 
identificateur x déjà défini dans c est masqué temporairement (liaison 
statique).
Remarque historique : si au lieu de masquer x, on le remplace, on obtient ce 
qui s'appelle la liaison dynamique (présente en Lisp). Ce principe contre­
intuitif remet en cause les bonnes propriétés du lambda­calcul 
(confluence, renommage, etc.) et n'est plus utilisé (sauf pour les 
méthodes dans les langages à objets, dans un cadre bien défini).
Programmation impérative
● L'impératif arrive avec les variables (pointeurs) et les 
effets de bord
– type 'a ref, val ref, !, :=
● Les structures de contrôle permettent de structurer le flot d'exécution de 
façon intuitive.
–  e ; e'
– while e do e done
– for x = e to e do e done
En fait, on pourrait les programmer de façon récursive, connaissant l'ordre 
d'évaluation de Caml
● Les types de données associés à la programmation impérative sont les 
tableaux et les structures modifiables “en place” (à champs mutables). 
Les fonctions ne sont pas des données dans un langage impératif.
● Certaines fonctions ne retournent rien et ne sont utiles que par leurs 
effets de bord. On les appelle des procédures.
Structures de contrôle
● Le contrôle “bas niveau” de l'exécution (goto, jump_if_zero), n'est 
plus utilisé pour programmer (sauf dans certaines applications très 
spécifiques (code optimisé pour des pilotes...) et aussi par certains 
vieux physiciens...
● En revanche, les return, break, continue de Java peuvent simplifier 
certains algorithmes.
● Les exceptions permettent également de remonter brutalement 
dans la structure de contrôle, mais elles permettent de plus de 
fournir une valeur exceptionnelle, qu'on peut utiliser ensuite.
Portée
● En caml, les références et les tableaux sont des constantes comme 
les autres (sauf que l'opération ! ne renvoie pas toujours le même 
résultat). La portée est donc toujours définie syntaxiquement 
comme l'expression “sous” le lambda ou le let ... in ...
● En C (et dans les autres langages impératifs), on a un tout petit 
peu moins de liberté : la portée peut être globale (tout le 
programme) locale à une fonction, à une boucle ou à un bloc 
(l'équivalent du let ... in ... : {int x ; ...}) le masquage d'une 
déclaration par une autre est restreint.
● Du point de vue de la programmation, il convient de toujours 
donner aux déclarations la plus petite portée possible.
Compilation des langages impératifs
● En simplifiant à l'extrême (on oublie les données et les 
optimisations) la compilation consiste à transformer les structures 
de contrôle de haut niveau en branchements élémentaires 
compréhensibles par le processeur :
i0 : condition
While condition ... : condition
 do corps i1 : jump_if_zero i2
i1+1 : corps
...     : corps
i2­1 : goto i1
i2 : suite

● Les tableaux sont représentés par des adresses contiguës et on 
calcule l'adresse d'une case en ajoutant son indice à l'adresse du 
tableau. De même pour les types structures.
Typage et gestion de la mémoire
Lambda­calcul simplement typé
(présentation informelle)
● Les types :
– types de base (int, bool) ou variables de types
– types flèches : si t, t' sont des types, on forme t  t'
● Les lambda­termes typés :
– variables (x, y)
– applications : e e' où e, e' sont des lambda­termes typés
– abstraction : x:t.e où t est un type et e un lambda­terme typé
● Jugements de type :
– Г ├ e : t où e est un terme, t un type et Г un contexte.
– (voir règles au tableau)
Sémantique
● Typage :
– les règles suivantes définissent le jugement x : t (x est de type t).
x :t ∈  , x : t ├ e :t '  ├ e :t t '  ├ e ': t
 ├ x :t  ├  x : t .e: t  t '  ├ ee ': t '

● Réduction :
– On choisi les mêmes règles que la bêta­réduction, ou bien une 
stratégie d'évaluation particulière.

la substitution e[e ' / x ]est licite e e' e e' e e'


  x : t.ee'  e[e ' / x ]  x : t . e  x : t . e' e e' '  e' e' ' e ' ' e e' ' e'

● Aucun programme bien typé ne se bloque
Well­typed programs never go wrong
● Les types assurent une certaine cohérence aux programmes. 
Dans un lambda­calcul enrichi, cela se traduit par un 
théorème sur la réduction :
– Un programme bien typé ne peut se réduire qu'en un programme 
bien typé (préservation).
– Un programme bien typé ne peut être bloqué (progrès). Par exemple, 
les programmes (1 2), (plus true 0) et (1 := 2) sont bloqués.
● Dans la réalité, le bloquage se transforme en des opérations absurdes 
consistant confondre des entiers et des adresses en mémoire, et inventer 
des adresses qui ne correspondent à rien. Le typage peut aussi servir à 
prévenir les accès non­autorisés à un système.
● La libération explicite de la mémoire (free) si elle est faite trop tôt, et la 
conversion de type (cast) cassent le système de types (formellement, la 
préservation). D'où une interdépendance avec le garbage collector.
● D'après la propriété de préservation, il suffit de vérifier le typage 
avant exécution, et on peut l'oublier ensuite : le typage est statique.
● Parfois, dans les vrais langages, le typage n'est pas entièrement 
statique : on doit mettre des contraintes explicites qui sont 
vérifiées à l'exécution (ce qui est coûteux car il faut garder et 
manipuler des types à l'exécution). C'est le cas en Java (à nuancer 
avec Java 1.5 et ses classes génériques) :
LinkedList s = new LinkedList();
s.add(new Element());
Element e = (Element) (s.getFirst());
● Une alternative : les types génériques pour les fonctions et les 
structures de données. Ces types sont beaucoup plus compliqués à 
vérifier, surtout si on veut faire de l'inférence.
– type 'a list = Nil | Cons of 'a * 'a list
– List.map : ∀'a∀'b ('a ­> 'b) ­> 'a list ­> 'b list
Systèmes de types
● De nombreux langages possèdent :
– Des types de base : int, char, string, bool
– Des types pointeurs, tableaux, fonctions
– La possibilité de définir de nouveaux types produits (structures)
● Un système de types est le plus utile lorsqu'il est inviolable. 
Le langage C au contraire possède un système de types non 
sûr (pour de raisons de performances et de simplicité). Java, 
Caml, Haskell ont des systèmes de types sûrs. Lisp est un 
langage non­typé.
Gestion de la mémoire
● Reprenons l'interpréteur avec contexte du lambda­calcul :
let rec eval c = function
| ...
| x  eval c (assoc c x)
| ...
● Lorsqu'on doit évaluer x dans un environnement c, on ne va pas recopier 
la valeur associée à x dans le c : on se contente de retourner la même 
“zone mémoire”, en espérant qu'elle ne sera pas effacée...
● En effet, dans ((a.a)(b.b))((c.c)(d.d)), il faut garder la valeur de c, à 
savoir (d.d), même après l'avoir dépilée du contexte. En revanche, à la 
fin, la valeur de a n'a plus d'intérêt.
Le ramasse­miettes
● La durée de vie d'un objet peut donc “dépasser” la portée des 
identificateur auxquels il peut être associé (il peut y en avoir 
plusieurs) lors de l'exécution.
● De plus, on ne peut pas décider statiquement si à tel point de 
programme un objet peut être libéré ou non (conséquence du 
théorème de Rice).
● On utilise donc un garbage collector : un programme qui, de temps 
en temps au cours de l'exécution du programme principal, regarde 
quels objets sont devenus inaccessibles, et les supprime pour 
récupérer de la mémoire. On sait que dans un langage muni d'un 
système de types sûr (propriété de préservation définie plus haut), 
un objet inaccessible l'est définitivement.
Programmation orientée objet
Objectif : associer données et traitements
● Les objets sont des valeurs qui contiennent à la fois
– un état (des données mutables)
– des méthodes qui font référence à l'état (de cet objet).
● On veut cacher les détails du fonctionnement interne (encapsulation)

class Counter {
    private int i;
    public Counter {i = 0;}
    public int get() {return i;}
    public void incr() {i++;}
}
● Une classe définit à la fois
– Un type d'objets
– Des moyens de construire des objets de ce type
– Leur comportement, de façon générique, au travers des méthodes.
Les éléments d'un langage objet

● Objets et classes : représentation de types de données abstraits
– Les objets : des super­ « records »
– Les classes : des modèles d'objets
● Héritage
– d'interfaces
– d'implémentation
● La liaison dynamique
– et la généricité
– et l'héritage
Composition d'un objet
● Des attributs d'instance (comme les champs d'un enregistrement)
– int n = 10 bool b = true, ...
– Object x = ...
● De méthodes d'instance (qui peuvent accéder aux attributs)
– void changeNumero(int m) {n = m} (affectation)
– bool pair() {return (n mod 2 == 0);}
– bool greater(Object o) {return (n >= o.n);}
– bool equals(Object o) {return (greater(o) && o.equals(this));}
– int factorielle(int i) {if (i == 0) return 1; else return i*factorielle(i­1);}
● L'idée est de rapprocher les données et leurs opérations associées.
Composition d'une classe
● Une classe est un type (comme un type enregistrement)
– types des attributs d'instance :
● int n, bool b, Object o, ...
– signatures des méthodes
● void changeNumero(int),  bool pair(), ...
● Une classe est un modèle
– initialisation des attributs par un constructeur
● MaClasse(n, b, o) {this.n = n, this.b = this.b, this.o = o}
– corps des méthodes (les attributs sont différents pour chaque objet)
● void changeNumero(int m) {n = m;}
Définition
En Java
Utilisation
class Counter = { Counter c1, c2;
c1 = new Counter(3);
int x; // un attribut c2 = new Counter(4);
c1.incr(2);
Counter(x) {this.x = x;} // un constructeur int v;
Counter() {x = 0;} // un autre constructeur v = c2.value();
...
int value() {return x;} // une méthode
void incr(int i) {x = x + i;} // une autre
}

Remarques :
● Les méthodes sont implicitement mutuellement récursives
● this désigne l'objet « courant »
● surcharge : dans une même classe, deux méthodes peuvent avoir le même 
nom, à condition d'être distinguables par les types de leurs arguments.
● En Java, tout objet est construit à partir d'une classe, qui est aussi son type.
Champs et méthodes d'instance et de classe

● Les attributs et méthodes « ordinaires » sont dits d'instance
– class C {... int x; ...} : un attribut x par objet de classe C
– class C {... int m(); ...} : une méthode m par objet de classe C

● Les attributs et méthodes de classe (appelés statiques en Java) 
existent en un seul exemplaire pour toute la classe.
– class C {... static int x; ...} : un seul x pour toute la classe
– class C {... static int m(); ...} : une seule méthode m
● Objets et modules sont bien adaptés à la description de types de 
données abstraits (ADT) : piles, arbres, tables, etc.. Dans la 
version orientée objet, toutefois, une version modifiable est plus 
souvent utilisée, pour correspondre au caractère impératif des 
langages à objets en général.

module List  = struct class List {
    type 'a t     public static nil = ...
    val nil : 'a t     public List cons(Object o) {...}
    val cons : 'a ­> 'a t ­> 'a t     public Object head() {...}
    val head : 'a t ­> 'a }
} fonctionnel

module List  = struct class List { impératif


    type 'a t     public List () {...}
    val empty : unit ­> 'a t     public void add(Object o) {...}
    val add : 'a ­> 'a t ­> unit     public Object head() {...}
    val head : 'a t ­> 'a }
}
modules objets
Liaison dynamique
● Que se passe­t­il lorsqu'on redéfinit une méthode qui était utilisée 
par une autre ? class CounterModulo extends Counter {
class Counter { void incr(int i) {x = x + i % 2;}
int x; }
void incr(int i) {x = x + i;}
void incr1() {incr(1);} Counter c = new CounterModulo();
} c.incr1() ; c.incr1();
● La liaison est dynamique : on ne connaît la méthode incr à appeler 
qu'au moment de l'exécution : c'est celle qui est définie dans la 
classe de l'objet c, c'est à dire CounterModulo. Autre exemple :

Counter [] counters = {new Counter(), new CounterModulo()};
for (i = 0 ; i < 2 ; i++) counters[i].incr();

● Il est impossible en général (indécidable), de savoir 
●  quelle méthode sera appelée effectivement.
Compatibilité et liaison dynamique
● Des objets qui partagent un certain nombre de méthodes (même 
nom, même signature) ont une certaine compatibilité :

class List { class Set {
    ...     ...
    public void add(Object o) {...}     public void add(Object o) {...}
} }

● L'intérêt des objets est qu'on peut appeler la méthode add d'un 
objet sans connaître son type exact (ce serait impossible avec des 
données simples et des fonctions add associées). Il s'agit d'une 
liaison dynamique (contrôlée).
Héritage
● Une interface en regroupe des signatures de méthodes qu'un objet doit 
posséder pour implémenter cette interface : c'est la notion de type pour 
les objets.
● L'héritage d'interface (sous­typage) est un moyen d'étendre une 
interface en ajoutant des méthodes ; les objets qui implémentent 
une sous­interface sont alors compatibles avec la super­interface :

interface Collection { interface List extends Collection {
    public void add(Object o);     public Object get(int Index);
    public bool isEmpty(); }
}
Héritage
● L'héritage d'implémentation (simple) permet d'étendre une classe en une 
autre en lui ajoutant des attributs et des méthodes. Il permet le partage de 
méthodes entre différentes classes : class RazCounter extends Counter {
class Counter {
void raz() {x = 0;}
int x;
}
void incr(int i) {x = x + i;}
} class ProgramCounter extends Counter {
Instruction [] prog;
Instruction next() {return prog[x];}
}
● Une classe peut utiliser les méthodes et attributs de sa super­classe
class UnCounter extends Counter {
void decr(int i) {incr(­ i);}
}
● Une classe peut redéfinir les méthodes de sa super­classe
class CounterModulo extends Counter {
void incr(int i) {x = x + i % 2;}
}
Héritage et liaison dynamique
● L'héritage d'implémentation est un bon moyen de réutiliser du 
code : une méthode héritée est partagée avec la super­classe (La 
liaison dynamique joue un rôle important).

class Collection { class List extends Collection {
    public abstract void add(Object o);     public void add(Object o) {...}
    public void addAll(Collection c) { }
        for(...) add(...); class Set extends Collection {
    }     public void add(Object o) {...}
} }
Compilation et exécution
● Contrairement au cas des langages impératifs, un appel de méthode dans 
un langage à objets ne peut pas toujours être résolu à la compilation 
(exemple de add). Il faut donc, à l'exécution, chercher la bonne méthode, 
qui peut se trouver dans la classe de l'objet, ou dans une super­classe.

● On peut considérer du point de vue du langage que chaque objet possède 
ses propres méthodes (celles­ci sont donc dupliquées autant de fois qu'il 
y a d'objets différents). Dans une implémentation efficace néanmoins, on 
ne crée qu'une seule fonction pour une classe et une méthode données, et 
l'objet this devient un argument supplémentaire.
Programmation logique
Programmation logique
Programmes logiques
● Une clause de Horn est une disjonction de formules 
atomiques dont au plus une est positive :
– p(f(x)) ∨ ¬ q ∨ ¬ r(f(x), g(x, f(x)))
● Une telle formule est logiquement équivalente à :
– p(f(x)) ⇐ q ∧ r(f(x), g(x, f(x)))
● Et on la note en Prolog :
– p(f(x)) :- q, r(f(x),g(x, f(x)))
● Un programme logique est un ensemble fini de clauses de 
Horn.
Exemples
est_triee(cons(X, cons(Y, L))) :­ inf(X, Y), est_triee(cons(Y, L)).
est_triee(nil).
est_triee(cons(X, nil)).

Pair(0).
impair(s(X)) :­ 
pair(X).
pair(s(X)) :­ 
impair(X).

plus(X, 0, X).
plus(X, s(Y), s(Z)) :­ plus(X, Y, 
Z).
La négation
● Il n'y a pas de vraie négation en prolog (impossible à 
définir avec des clauses de Horn).
● On peut cependant y parvenir en essayant de prouver un 
but et en vérifiant qu'il n'y a pas de solution. C'est la 
négation par l'échec.
● Évidemment, le prédicat de départ doit être fait pour 
terminer même s'il n'y a pas de solution.
Exécution
● Pour exécuter un programme Prolog, on se donne un but, qui 
est une conjonction de formules atomiques (non closes) :
– plus(s(0), s(s(0)), X)
– Ou encore pair(X)
● L'interpréteur retourne alors une (ou des) substitution(s) qui 
rend(ent) ce but prouvable (s'il en existe).
– X = s(s(s(0)))
– X = 0, X = 2, X = 4, ...
Sémantique déclarative
● Un programme logique définit une théorie (l'ensemble des 
conséquences des formules du programme).
● Pour un but donné, un programme logique peut boucler, ou bien 
s'arrêter. Dans les deux cas, il peut rendre zéro ou plusieurs 
substitutions.
– Toute substitution rendue par le programme (même s'il boucle 
après) fait du but une conséquence de la théorie.
– Si le programme termine, alors toutes les conséquences qui ont 
la forme du but ont été énumérées.
Sémantique opérationnelle
● Prolog procède par résolution linéaire directe pour les clauses de 
Horn. C'est une méthode de preuve correcte et complète, d'où les 
deux propriétés de la sémantique déclarative.
● Principe :
– On commence par chercher la première clause du programme 
dont la tête s'unifie avec le but courant. On “applique” alors la 
substitution obtenue à la queue de la clause
– On cherche alors à prouver (de gauche à droite) les prémices 
de cette clause (qui deviennent tour à tour le but courant).
– Lorsqu'on est bloqué dans une impasse, on revient en arrière, 
pour essayer une autre clause.
Ce faisant, on construit (par les unifications successives) une 
substitution qui rend le but prouvable.
Intérêt et défauts
● Prolog est un langage adapté à la recherche de solutions 
(Sudoku ?), pour faire de l'inférence de type, etc.
● Les prédicats du programme jouent le rôle de fonctions, qu'on peut 
appeler de différentes façons : plus peut servir pour l'addition et la 
soustraction, selon l'instanciation des paramètres.
● L'impératif s'intègre plutôt mal (car l'ordre d'évaluation est 
difficile à prévoir), ce qui peut poser problème pour les entrées­
sorties et l'interface avec d'autres langages.
● L'absence de déclaration préalable des symboles de fonctions et de 
prédicats est une source importante d'erreurs (Il existe des versions 
typées de Prolog cependant).
Conclusion :
les défis de la programmation aujourd'hui
● La programmation parallèle, qu'on n'a pas abordée, est 
souvent très délicate et suscite également à elle seule de 
nombreux paradigmes de programmation (passage de 
messages ou partage de données, etc.). Les besoins sont 
croissants (grappes de processeurs, processeurs à 
plusieurs coeurs).
Conclusion :
les défis de la programmation aujourd'hui
● Le génie logiciel : programming in the large. Écrire de 
gros programmes pose depuis des décennies des 
problèmes fondamentalement différents : organisation, 
évolution (maintenance), réutilisation
● La maîtrise de la programmation orientée objet, 
générique, par composants, par aspects... aide à résoudre 
ces problèmes.
Conclusion :
les défis de la programmation aujourd'hui
● La sûreté de fonctionnement des logiciels est loin d'être 
une question résolue. Des techniques élaborées de test, 
d'analyse automatique, voire de preuve formelle avec un 
assistant, sont autant de domaines de recherches. Bien 
entendu, les paradigmes, et mêmes les langages de 
programmation utilisés influencent beaucoup les 
possibilités.
Un peu de lambda­calcul
● Syntaxe du lambda­calcul (on ajoutera des parenthèses quand 
c'est nécessaire pour lever les ambiguïtés)
e = | x (identificateur)
| x.e (lambda­abstraction)
| ee (application)
Exemple : x.y.(xy) (en caml : fun x ­> fun y ­> x y)
● Occurrences libres
Dans (x.x) x, le second x est une occurrence liée de 
l'identificateur x ; le troisième est une occurrence libre.
● Remplacement
On note e[e' / x] le terme obtenu en remplaçant simultanément 
toutes les occurrences libres de x par e' dans e
Exemple : (x.x)x [(y.yy) / x] = (x.x)(y.yy)
Une substitution est licite si aucune variable libre du terme e' n'est 
capturée par un lambda de e
Exemple : (x.y) [x / y] n'est pas licite.
● Alpha­équivalence (renommage)
si y n'a pas d'occurrence libre dans e alors x.e ~ y.e[y / x]
si e ~ e' alors x.e ~ x.e'
si f ~ f' et e ~ e' alors f e ~ f' e'
on prend la fermeture symétrique réflexive transitive
L'alpha­équivalence permet de renommer un identificateur, en 
évitant le phénomène de capture :
x.y.xy est équivalent à x.z.xz,
mais pas à x.x.xx (car x a une occurrence libre dans xy)
● Bêta­réduction
si la substitution e[e' / x] est licite alors (x.e) e'  e[e' / x]
si e  e' alors x.e  x.e'
si e  e' alors e e''  e' e'' et e'' e  e'' e'
● Exemple :
((x.y.xy) (x.x)) t  (y.(x.x)y) t  (x.x)t  t
ou bien    (y.(x.x)y) t  (y.y)t  t
● Confluence :
L'ordre de réduction ne change pas le “résultat” : il ne peut y avoir 
qu'une seule forme irréductible d'un lambda­terme (mais parfois 
certains chemins peuvent terminer et d'autres non).
● Le lambda­terme suivant se réduit indéfiniment.
(x.xx)(x.xx)  (x.xx)(x.xx)  ...

Вам также может понравиться