• Aucun résultat trouvé

Programmation. fonctionnelle, impérative, logique et orientée objet

N/A
N/A
Protected

Academic year: 2022

Partager "Programmation. fonctionnelle, impérative, logique et orientée objet"

Copied!
62
0
0

Texte intégral

(1)

Programmation

fonctionnelle, impérative, logique et orientée objet

(2)

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

(3)

Processeur Compilateur (gcc, ocamlopt)

Programme Programme

machine

Processeur Programme machine

Entrées Sorties

Compilation (C, Caml,  Fortran...)

Interprétation (Lisp)

Processeur Interpréteur

Programme +

entrées

Sorties

Le programme  compilé est 

très efficace

peu efficace

(4)

Java

int k = 1, l = 1;

while (k <= i) {   l = l * k;

  k++;

}

return l;

Bytecode    0:   iconst_1    1:   istore_2    2:   iconst_1    3:   istore_3    4:   iload_2    5:   iload_0

   6:   if_icmpgt       19    9:   iload_3

   10:  iload_2    11:  imul    12:  istore_3    13:  iinc    2, 1    16:  goto    4    19:  iload_3    20:  ireturn

Exemple de compilation : factorielle

(5)

Processeur Compilateur

Programme Programme

“bytecode”

Compilation pour une

machine virtuelle

(Java, Caml,  .Net)

Processeur Y Machine virtuelle Y

Programme

“bytecode”

+ entrées

Sorties

Processeur X Machine virtuelle X

Programme

“bytecode”

+ entrées Sorties

Le programme 

est portable et 

moyennement  

efficace

(6)

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.

(7)

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...)

(8)

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

(9)

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”.

(10)

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)

Extensions du lambda­calcul

(11)

Programmation fonctionnelle

(12)

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).

(13)

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.

(14)

Bêta­réduction et ordre de réduction

Bêta­réduction : réduction non déterministe

Caml : les arguments d'abord

Évaluation paresseuse (Haskell) :

la substitution e[e '/x]est licite

x.ee'e[e'/x]

ee'

x.e x.e'

ee' e e' 'e' e' '

ee' e ' ' ee' ' e'

x.ee'e[e '/x]

ee' e e' 'e' e' '

ee' e ' ' ee' ' e'

prioritaire par rapport  aux deux autres règles

x.e est une  forme normale

x.ee'e[e '/x]

ee' e e' 'e' e' '

x.e est une  forme normale

(non spécifié officiellement)

remplacement  sans recopie !

(15)

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.

(16)

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

(17)

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 *)

(18)

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).

(19)

Programmation impérative

(20)

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.

(21)

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.

(22)

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.

(23)

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 :

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.

While condition  do corps

i0 : condition ... : condition

i1 : jump_if_zero i2 i1+1 : corps

...     : corps

i2­1 : goto i1

i2 : suite

(24)

Typage et gestion de la mémoire

(25)

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)

(26)

Sémantique

Typage :

les règles suivantes définissent le jugement x : t (x est de type 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.

Aucun programme bien typé ne se bloque

x :t∈

x :t

, x : te :t '

x : t.e: tt '

e :tt ' e ': t

ee ': t '

la substitution e[e '/x]est licite

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

ee'

x : t.e x : t.e'

ee' e e' 'e' e' '

ee' e ' ' ee' ' e'

(27)

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.

(28)

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

(29)

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é.

(30)

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.

(31)

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.

(32)

Programmation orientée objet

(33)

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)

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.

class Counter {     private int i;

    public Counter {i = 0;}

    public int get() {return i;}

    public void incr() {i++;}

}

(34)

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

(35)

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.

(36)

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;}

(37)

En Java

class Counter = {

int x; // un attribut

Counter(x) {this.x = x;} // un constructeur Counter() {x = 0;} // un autre constructeur int value() {return x;} // une méthode

void incr(int i) {x = x + i;} // une autre }

Counter c1, c2;

c1 = new Counter(3);

c2 = new Counter(4);

c1.incr(2);

int v;

v = c2.value();

...

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.

Définition Utilisation

(38)

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

(39)

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.

class List {

    public static nil = ...

    public List cons(Object o) {...}

    public Object head() {...}

} module List  = struct

    type 'a t     val nil : 'a t

    val cons : 'a ­> 'a t ­> 'a t     val head : 'a t ­> 'a

}

module List  = struct     type 'a t

    val empty : unit ­> 'a t     val add : 'a ­> 'a t ­> unit     val head : 'a t ­> 'a

}

class List {

    public List () {...}

    public void add(Object o) {...}

    public Object head() {...}

}

objets modules

fonctionnel

impératif

(40)

Liaison dynamique

Que se passe­t­il lorsqu'on redéfinit une méthode qui était utilisée  par une autre ?

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 :

Il est impossible en général (indécidable), de savoir 

 quelle méthode sera appelée effectivement.

class CounterModulo extends Counter { void incr(int i) {x = x + i % 2;}

class Counter { } int x;

void incr(int i) {x = x + i;}

void incr1() {incr(1);}

}

Counter c = new CounterModulo();

c.incr1() ; c.incr1();

Counter [] counters = {new Counter(), new CounterModulo()};

for (i = 0 ; i < 2 ; i++) counters[i].incr();

(41)

Compatibilité et liaison dynamique

Des objets qui partagent un certain nombre de méthodes (même  nom, même signature) ont une certaine compatibilité :

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).

class List {     ...

    public void add(Object o) {...}

}

class Set {     ...

    public void add(Object o) {...}

}

(42)

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 {

    public void add(Object o);

    public bool isEmpty();

}

interface List extends Collection {     public Object get(int Index);

}

(43)

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 :

Une classe peut utiliser les méthodes et attributs de sa super­classe

Une classe peut redéfinir les méthodes de sa super­classe

class Counter { int x;

void incr(int i) {x = x + i;}

}

class RazCounter extends Counter { void raz() {x = 0;}

}

class CounterModulo extends Counter { void incr(int i) {x = x + i % 2;}

}

class ProgramCounter extends Counter { Instruction [] prog;

Instruction next() {return prog[x];}

}

class UnCounter extends Counter { void decr(int i) {incr(­ i);}

}

(44)

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 {

    public abstract void add(Object o);

    public void addAll(Collection c) {         for(...) add(...);

    } }

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

}

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

}

(45)

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.

(46)

Programmation logique

(47)

Programmation logique

(48)

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.

(49)

Exemples

Pair(0).

impair(s(X)) :­ 

pair(X).

pair(s(X)) :­ 

impair(X).

est_triee(cons(X, cons(Y, L))) :­ inf(X, Y), est_triee(cons(Y, L)).

est_triee(nil).

est_triee(cons(X, nil)).

plus(X, 0, X).

plus(X, s(Y), s(Z)) :­ plus(X, Y, 

Z).

(50)

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.

(51)

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, ...

(52)

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.

(53)

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.

(54)

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).

(55)

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).

(56)

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.

(57)

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.

(58)

Un peu de lambda­calcul

(59)

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)

(60)

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.

(61)

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)

(62)

Bêta­réduction

si la substitution e[e' / x] est licite alors (x.e) e'  e[e' / x]

si  e' alors x.e  x.e'

si  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)  ...

Références

Documents relatifs

Si l'on appel notre méthode publique Animaux() depuis une instance de $zoo, cela va retourner de manière sécurisée notre tableau d'animaux stocké dans notre classe, et personne ne

une commande est créée pour un client et un catalogue donnés, on peut ajouter des articles à une commande, accéder à la liste des articles commandés ainsi que prix total des

L’interpréteur java permet d’exécuter une application écrite en langage java (autre qu’une applet), plus spécifiquement un fichier ClassName.class (i.e le java bytecodes). Par

Une interface est introduite par le mot clé « interface », et se comporte comme une classe dont toutes les méthodes sont abstract et dont tous les attributs sont final. Les mots

Additionner deux vecteurs donnés ; on écrira pour cela une méthode statique nommée aussi addition (en utilisant ainsi la possibilité de la surcharge) qui recevra en

– Pour qu’une méthode accède aux variables déclarées dans la classe, elle doit y faire appel à l’aide de la syntaxe suivante :.

Hors cette étape n'est clairement pas du ressort d'un objet : seule la classe possède suffisamment d'informations pour la mener à bien : la création d'un objet est donc une méthode

Les exemples de code sont écrits en C#, mais sont facilement transposables à d'autres langages orientés objets comme