INFO421 : Programmation fonctionnelle

De Wiki du LAMA (UMR5127)

Ce wiki est un complément de cours pour le cours « info-421 : programmation fonctionnelle ». La participation au wiki est fortement encouragée, et deviendra peut-être obligatoire...

Pour pouvoir modifier les pages, inscrivez-vous (lien en haut à droite) pour obtenir un login et mot de passe. (Utilisez votre vrai nom...)

Je vous conseille d'aller voir ce guide pour vous familiariser avec les wikis.


Exercice : si vous n'en avez pas, créez-vous un compte et essayez de modifier cette page (correction de fôtes d'aurtograffe, rajout de détails, mise en page, ...)

Vous pouvez aussi utiliser la page de discussion pour ... discuter. (Ou poser des questions, faire des commentaires etc.)


Sommaire

Détails techniques

Compléments de cours, TD et TP

Installer Ocaml

Si vous voulez installer OCaml sur votre ordinateur :

  • Sous Linux : c'est la solution idéale. Il existe probablement des paquets pour votre distribution. Pour Ubuntu, pour avoir un environnement similaire à ce que vous aurez dans les salles informatiques, installez les paquets ocaml, ocaml-core, ocaml-mode, tuareg-mode et emacs.
  • Sous MacOS : il semblerait qu'il y ait des paquets MacPorts pour ocaml, emacs et tuareg-mode.el.
  • Sous Windows : je vous renvoie au tutoriel de Jean-Paul Roy : Installation de OCaml (sur Windows XP). Je n'ai malheureusement (??) pas accès à une machine avec Windows (98/2000/XP/Vista/7), je ne pourrais donc pas beaucoup vous aider.

Contactez moi si vous avez des problèmes.

Pour les exemples simples, vous pouvez aussi utiliser OCaml en ligne

Références supplémentaires

Cours

TD et TP

Introduction

Pour paraphraser un collègue dont je ne retrouve pas le nom :

Attention : il ne s'agit pas d'un cours de programmation fonctionelle

Il s'agit plutôt d'un cours de programmation fonctionnelle...

Petit historique censuré

Je ne parlerais pas des langages pré-historiques (cartes perforées, λ-calcul, machines de Turing, ...)

D'après ce site, qui recense la plupart des langages de programmation, il y aurait plus de 2500 langages ! Voici donc une petite liste de langages importants :

  • années 40 : langages d'assemblage (assembleurs). Aussi vieux que les ordinateurs eux-mêmes. Chaque langage d'assemblage est spécifique à une famille de processeurs, ce qui rend les programmes difficiles à porter. (Càd à modifier pour les faire marcher sur d'autres ordinateurs.)
  • FORTRAN (1957, toujours utilisé par les numériciens et physiciens) et COBOL (1960, toujours utilisé en gestion). Ces langages ont connus des évolutions mais restent archaïques par leur conception.
  • LISP : inventé par John McCarthy en 1958. C'est le premier langage fonctionnel. Toujours utilisé (sous différentes formes), en particulier en intelligence artificielle. Ce langage est basé directement sur le λ-calcul de Church.
  • ALGOL (1958, a inspiré de nombreux langages depuis : C, pascal, ...) Le but était de réparer certains défauts des langages de type FORTRAN. (Programmation structurée, blocs, ...)
  • Pascal (1975).
  • C (1972). Toujours très utilisé, sous différentes variantes (notamment C++).
  • Prolog (1972) : programmation logique, paradigme nouveau de programmation. Toujours utilisé par une petite communauté.
  • ML (fin des années 1970 ?), qui ajoute une notion de type que LISP n'avait pas.
  • Smalltalk (fin des année 1983), début de la programmation objet.
  • 1983 : ADA.
  • année '80 : Caml (1987), puis CamlLight(1990), puis OCaml(1996), développé

à l'INRIA. (Dernière version en octobre 2012.)

  • années '90 : Python (version 1.0 en 1994).
  • années '90 : PHP (version 1.0 en 1995).
  • années '90 : Java (version 1.0 en 1996).

Je vous conseille d'aller voir le graphique suivant : Computer Languages Timeline (ou découpé en pages A4).

Fonctionnel ??

L'adjectif fonctionnel a au moins deux sens :

  1. qui fonctionne, en état de marche,
  2. qui se rapporte aux fonctions.

Les langages fonctionnels sont bien entendus fonctionnels dans le premier sens, mais c'est surtout le second sens qui nous intéresse. Les langages tels que Pascal, Ada ou C sont qualifié, par opposition, d'impératifs.

Un des slogans de la programmation fonctionnelle en général est

Les fonctions sont des valeurs comme les autres

et c'est de là que vient la terminologie... En particulier, il n'y a pas de différence entre les instructions (qui ont un effet) et les expressions (qui ont une valeur). Par exemple, en Python,

x = x+1

ou

if delta < 0:
    print("Il n'y a pas de solution")

sont des instructions : on ne peut pas les mettre dans une variable.

Comme nous le verront, cela a des conséquences sur l'expressivité du langage et la manière de programmer.

Le langage (O)Caml

Le langage OCaml est développé par l'INRIA (Institut national de recherche en informatique et automatique). C'est un successeur de CamlLight.

Le nom Caml est formé des initiales de "Categorical Abstract Machine Language", et le langage lui même appartient à la famille de ML ("Meta Language"). C'est un langage fonctionnel strict (nous verrons ce que cela veut dire), statiquement typé (nous verrons ce que cela veut dire) qui supporte plusieurs styles de programmation :

  • fonctionnel bien sûr,
  • mais aussi impératif,
  • objet également (c'est le « O » de OCaml).

Dans ce cours, nous utiliserons principalement le style fonctionnel, et un peu le style impératif en fin de semestre.

Voici quelques aspects importants du langages que nous essayeront d'aborder pendant le cours :

  • fonctions comme valeurs,
  • types de données algébriques
  • polymorphisme,
  • système d'exceptions,
  • support pour des références et des données mutables (programmation « impure »),
  • système de modules et de foncteurs,
  • ...

La notion de récursivité sera fondamentale pendant toute la durée du cours...

Un aspect intéressant du langage est que c'est :

  • un langage interprété (avec l'interpréteur OCaml),
  • soit un langage compilé en bytecode (code binaire indépendant de l'architecture),
  • soit un langage compilé optimisé en binaire (dépendant de l'architecture).

Autres langages fonctionnels

Il existe de nombreux autres langages fonctionnels. Par exemple :

  • SML (Standard ML) : Wikipedia, autre dialecte de la famille ML.
  • LISP, dont les deux dialectes principaux sont :
  • Haskell : Wikipedia (inspiré en grande partie de Miranda).

Plusieurs langages impératifs intègrent maintenant des aspects propres des langages fonctionnels : Python, Scala, ...

Applications concrètes

Voici quelques exemples de logiciels développés en OCaml :

La viabilité du paradigme fonctionnel se retrouve également dans le langage Erlang (Wikipedia), un langage fonctionnel développé par Ericsson pour la programmation concurrente de systèmes temps réels.

Objectifs du cours

  1. être capable de définir des fonctions récursives, et comprendre ce qu'elles font
  2. comprendre le typage, les types algébriques et le polymorphisme à la ML
  3. pouvoir définir des fonction d'ordre supérieur pour modulariser votre code
  4. être capable de décomposer un problème
  5. commencer à réfléchir à la complexité de vos programmes

Premiers pas en Caml

Un des slogans de la programmation fonctionnelle est tout est une valeur, ou plus précisément,

Toute expression a une valeur.

Cette idée n'est pas présente en Python où l'on distingue les expressions ("3*x+f(2)" par exemple) et les instructions ("if n>22: a=12 else: a=13" par exemple). En Python, les instructions sont en général séparées par des retours à la ligne et sont exécutées en séquence. Les expressions sont juste calculées, et ne peuvent pas être séquentialisées.

Dans un langage purement fonctionnel, le ";" n'existe pas, et il n'y a que des valeurs...

Les valeurs sont formées en utilisant :

  • les fonctions du langage (addition, multiplication, opérateurs logiques),
  • des constructions du langage (if ... then ... else ... par exemple),
  • les relations mathématiques (égalité, plus grand), ... qui sont juste des fonctions renvoyant des booléens,
  • les constantes du langage,
  • les variables de l'environnement,
  • les valeurs (en particulier les fonctions) définies par le programmeur.

Bien entendu, comme Caml est typé, il faut respecter les types.

Les types atomiques

Caml (et les autres langages fonctionnels typés) possèdent plusieurs types de base tels que :

  • les booléens : type bool (valeur true et false) et les opérations not, && (et) et || (ou).
  • Les entiers : type int pour les entiers signés compris entre − 2 < sup > 30 < / sup > et 2 < sup > 30 < / sup > − 1. (Les entiers Caml sont stockés sur 31 bits...) Les opérations associés sont +, -, *, / (pour la division entière) et mod (pour le modulo).
  • Les flottants : type float pour les réels en virgule flottante. Les opérations usuelles sont +., -., *. et /..
  • Les caractères : type char (notés entre guillemets simples).
  • Le type unité : type unit, dont l'unique valeur est (). (Nous verrons que ce type a une utilité...)

Même s'il ne s'agit pas vraiment d'un type atomique, nous pouvons également ajouter le type des chaînes de caractères : type string (notées entre guillemets doubles).

(* exemples ... *)

Remarques : les opérations booléennes ("&&" pour le « et » et "||" pour le « ou ») fonctionnent de à gauche à droite, et l'argument de droite n'est évalué que si c'est nécessaire. Par exemple true || (0 = 1/0) donne la valeur true, alors que que false || (0=1/0) lève l'exception Division_by_zero.

définitions et environnement

Comme tous les langages de programmation, Caml garde en mémoire une liste de définitions. Chacune de ces définitions met en relation un nom ("x", "n", "length", ...) et une valeur ("3.7", "42", "fun s -> ...").

Lorsque l'on lance Caml, l'environnement contient déjà de nombreuse fonctions prédéfinies : max, min, mod, +, float_of_int, ...

Certaines des variables de l'environnement correspondent à des paramètres de fonction en cours de définition. Ces variables n'ont pas de valeur, mais seulement un type.

Environnement

  • Liste de paires nom / valeur,
  • une nouvelle definition remplace la précédente mais ne l'efface pas, (on ne met pas une valeur à jours)

Définitions

L'utilisateur peut rajouter des définitions dans l'environnement grâce au mot clé let

  • let nom = expr permet de rajouter une définition simple dans l'environnement. Exemple : let n = 42
  • let nom1 = expr1 and nom2 = expr2 permet de rajouter plusieurs définitions simultanément. Exemple : let a=1 and b=2
  • let rec nom1 = expr1 and nom2 = expr2 permet de rajouter des définitions mutuellement récursives.

On peut toujours annoter la définition de types de données. Ceci évite à Caml d'avoir à deviner le type que l'on souhaite et permet de préciser la fonction :

 let n:int = 42
 let rec f (n:int) : string = if (n<1) then "" else "*" ^ (f (n-1))

Si une définition utilise le même nom qu'une définition précédente, la nouvelle définition écrase l'ancienne. Par exemple :

 # let n=42 ;;
 val n : int = 42
 # let n=3.1415 ;;
 val n : float = 3.1415
 # n ;;
 
* : float = 3.1415

Définitions locales

Il est possible de modifier l'environnement de manière temporaire (locale). On utilise alors let nom = expr1 in expr2. Ceci ajoute la définition nom = expr1 dans l'environnement, mais seulement pour l'évaluation de l'expression expr2.

# let x =
  let y = 42 in
    y/2 ;;
val x : int = 21
# x ;;
 
* : int = 21
# y ;;
Unbound value y

Les fonctions

Caml utilise la notation mathématique pour écrire les types des fonctions. Si f est une fonction avec un seul argument de type string et dont la valeur de retour est de type int (la fonction length par exemple), le type de f est noté string -> int. Dans l'interprète Caml :

 # String.length ;;
 
* : string -> int = <fun>

Nous indique que la fonction length est bien de type string->int.

Une fonction est définie comme une variable avec un let et en utilisant le mot clé fun :

# let f = fun n -> n+1;;
val f : int -> int = <fun>
# let racine = fun n -> int_of_float (sqrt (float_of_int n));;
val racine : int -> int = <fun>

Une fonction avec plusieurs argument est définie de la même manière avec

# let moyenne3 = fun x y z -> (x +. y +. z) /. 3. ;;
val moyenne3 : float -> float -> float -> float = <fun>

Le type d'une fonction à plusieurs argument est noté en mettant plusieurs types à gauche de la flèche ->, comme ci dessus : type_arg1 -> type_arg2 -> type_arg3 -> type_resultat.

Remarques :

  • notez l'absence de mot clé return et l'absence de virgules pour séparer les différents arguments !
  • Le mot clé fun permet de définir des fonctions sans leur donner de nom, ce qui n'est pas possible dans les langages comme C ou Java...

On peut également utiliser un raccourci pour définir une fonction sans utiliser le mot clé fun : il suffit de mettre les arguments de la fonction avant le signe égal :

# let f n = n+1;;
val f : int -> int = <fun>
# let racine n = int_of_float (sqrt (float_of_int n));;
val racine : int -> int = <fun>
# let moyenne3 x y z = (x +. y +. z) /. 3. ;;
val moyenne3 : float -> float -> float -> float = <fun>

Pour appliquer une fonction à des arguments, on écrit simplement

moyenne3 13.5 17.0 -0.333

Il n'y a ni parenthèse, ni virgule. Les parenthèses peuvent cependant être nécessaires pour différencier

moyenne3 13.5 17.0 (-0.333 *. 6.)

et

(moyenne3 13.5 17.0 -0.333) *. 6.

ou pour que Caml lise correctement

moyenne3 13.5 (17.0 -. 12.0) -0.333

Fonctions récursives

Si on essaie de définir une fonction récursivement :

let f n = if (n<1) then 0 else n + f (n-1)

Caml répond

Error: Unbound value f

car f n'est pas présente dans l'environnement.

Pour rajouter f comme variable active (sans valeur) et pouvoir faire une définition récursive, il faut utiliser le mot clé rec :

# let rec f n = if (n<1) then 0 else n + f (n-1);;
val f : int -> int = <fun>

Les définitions locales peuvent elles aussi être récursives (let rec ... in ...) ou mutuellement récursives (let rec ... and ... in ...). Elles acceptent également les annotations de type...

fonctions d'ordre supérieur

TODO

Les listes

Le type des listes est fondamental en programmation fonctionnelle. D'une certaine manière, on peut dire qu'ils remplacent les tableaux que l'on utilise constamment en programmation impérative.

Exercice : quels sont les différences importantes (utilisation, complexité, mémoire, ...) entre les tableaux et les listes ?

En Caml, une liste est représentée entre crochets, et les éléments sont séparés par des points-virgules :

 # let une_liste = [ 1 ; 2 ; 2 ; -1 ; 0 ] ;;
 val une_liste : int list = [1; 2; 2; -1; 0]
 # let une_autre_liste = [ "coucou" ; "je m'appelle" ; "Bob" ] ;;
 val une_autre_liste : string list = ["coucou"; "je m'appelle"; "Bob"]

Comme vous pouvez le voir dans l'exemple au dessus, le type des listes d'entiers s'appelle "int list", le type des listes de flottants s'appelle "float list" et le type des listes de listes d'entiers s'appelle ... "int list list".

Contrairement au cas des tuples, les éléments d'une liste doivent tous avoir le même type :

 # let une_non_liste = [ 1 ; "Toto" ; 1.3 ] ;;
 This expression has type string but is here used with type int

Attention : quel est le type de la liste suivante ?

 # let une_liste_bizarre = [ 1 , 2 , 3 ] ;;

On peut ajouter un élement devant une liste avec l'opérateur "::" et la concaténation des listes s'obtient avec l'opérateur "@".

# 5::[1;2;3;4] ;;
- : int list = [5; 1; 2; 3; 4]
# [-1;-2;-3;-4] @ [1;2;3;4] ;;
- : int list = [-1; -2; -3; -4; 1; 2; 3; 4]

Attention : la complexité de l'opération "::" ne dépend pas de la taille des listes mises en jeux. Par contre, la complexité de l'opération "@" dépend proportionnellement de la taille de son premier argument. Il faut donc toujour privilégier "::" par rapport à "@". En particulier, il est en général mauvais de rajouter des élément en fin de liste en utilisant "l @ [e]".

Filtrage, première partie : tuples et listes

La notion de filtrage, aussi appelé "pattern-matching" est un outils clé pour la programmation en Caml. L'idée est de décomposer une valeur en "sous-valeurs".

Filtrage sur les tuples

Comme une paire est de la forme (x,y), on peut décomposer une valeur de type a*b en deux sous valeurs. Voici un exemple de syntaxe : le code de la fonction fst :

  let premier p = match p with
    (x,y) -> x

Pour évaluer "premier e", Caml regarde le premier argument e et essaie de faire coïncider "(x,y)". S'il y arrive, alors il renvoie la valeur "x". Le typage de Caml inférera automatique que "p" doit être une paire, et il n'y a donc pas besoin de prévoir un cas par defaut.

On peut maintenant récupérer la deuxième composante d'un triplet de la manière suivante :

  let second3 t = match t with
    (_, y, _) -> y

Notez l'utilisation de "_" pour donner la position d'une sous-valeur qui ne sert à rien.

Toutes les variables présentes dans le motif (la partie à gauche de la flèche) se retrouvent dans l'environnement avec pour valeur, la sous-valeur appropriée. Attention, ces variables ne se retrouvent dans l'environnement que dans la partie droite de la flèche. Ces variables remplacent (temporairement) les variables de même nom qui pouvaient se trouver dans l'environnement. Par exemple, la fonction :

  let f x = match x with
   (x,_) -> x

est exactement la fonction fst.

Filtrage sur les listes

On peut décomposer une liste en deux sous-valeurs :

  • le premier élément de la liste,
  • la suite de la liste.

Autrement dit, si la liste l n'est pas vide, on peut la décomposer en e::q. Comme il est possible que la liste l soit vide, il y a en fait deux manière de décomposer l :

  • soit [] (sans aucune sous-valeur),
  • soit e::q (avec deux sous-valeurs).

Par exemple, voici une manière de tester si une liste est vide :

 let vide l = match l with
     [] -> true
   | x::xs  ->  false

Les différents cas sont séparés par le symbole "|".

On peut également décomposer une liste en 3 sous-valeurs : le premier élément, le second élément et la suite de la liste. Dans ce cas, il a trois cas pour une liste arbitraire :

  • elle est vide [] (aucune sous-valeur),
  • elle ne contient qu'une seul élément [e] (une sous-valeur),
  • elle contient un premier élément, un deuxième élément, et une suite a::b::q (trois sous-valeurs).

On peut par exemple écrire :

let deux_elements_ou_plus l = match l with
    [] -> false
  | [a] -> false
  | a::b::q -> true

Comme on peut toujours décomposer une valeur en une sous-valeur (la valeur elle même), on peut toujours avoir un motif de la forme x. Toutes les valeurs pourront être décomposées de cette manière, et il faut donc que ce motif soit le dernier. (Sinon, les motifs suivants ne servent à rien...) On parle de motif universel. (On peut utiliser _ comme motif universel.) Par exemple, on peut réecrire l'exemple précédent :

let deux_elements_ou_plus
    _::_::_  -> true
  | _ -> false

Compléments

  1. On peut ajouter des constantes dans les motifs. Le motif ne sera utilisé que lorsque la sous-valeur correspondantes a la valeurs correspondante. Par exemple, si on modélise les entiers de Gauss (« entiers complexes ») par des paires d'entiers, on peut définir :
  let imaginaire_pur c = match c with
    (0,0) -> false
  | (0,_) -> true
  | _ -> false
  1. On peut grouper des motifs différents pour faire la même chose. Il faut que les variables qui apparaissent dans les motifs soient les mêmes, et qu'elles aient le même type :
let f x = match x with
    0::l | 1::_::l | 2::_::_::l  -> l  (* on groupe 3 motifs en une seule ligne *)
  | [] -> []
  | x::_ -> [x]
  1. on peut « garder » les motifs" par une condition avec le mot clé "when"
let g x = match x with
    x::xs  when (List.lenght (f xs) = 1)  ->  true
  | _ -> false

Attention dans le cas où vous utilisez when : il est conseillé de terminer votre filtrage avec un motif universel pour être sûr que vous n'avez pas oublié de cas.

  1. comme on définit souvent des fonctions par filtrage, Caml autorise la syntaxe
function
    pattern1 -> expr1
  | pattern2 -> expr2
  | ...

plutôt que

fun x -> match x with
    pattern1 -> expr1
  | pattern2 -> expr2
  | ...

Remarquez que l'on ne peut faire ça que pour une fonction à une seule variable...

  1. fonction par filtrage : s'il n'y a qu'un seul motif, on peut alors utiliser le mot clé "fun" habituel, ce qui permet la définition d'une fonction à plusieurs arguments. (Mais dans ce cas, les parenthèses autour des tuples sont obligatoires.)
# let h = fun (x,y) z (u,v,w)  ->  x+y+z+v ;;
val h : int * int -> int -> 'a * int * 'b -> int = <fun>

Le polymorphisme

Transparence référentielle

Les types algébriques

On peut défnir un synonyme de type avec le mot clef type Exemple :

type point = float*float

Types énumérés

On donne tous les éléments du type Exemple :

type jours = Lundi | Mardi | Mercredi | ...

Type avec 7 éléments. Les éléments commencent forcément par une majuscule.

On peut comparer des éléments avec "=", "<" et ">" (< et > sont à éviter, il vaut mieux écrire une fonction pour comparer) On peut aussi faire du filtrage.

Exemple:

let week_end j = match j with
     Samedi -> true
   | Dimanche -> true
   | _ -> false

Construction de types paramètres

Exemple : type des transformations de l'espace avec : - Translation - Rotation - Symetrie On aura alors des constructeurs avec des paramètres :

type transformation = Rotation of int*point
                    | Translation of point
                    | SymCentrale of point
                    | Homothetie of float*point

On n'a pas donné explicitement tous les éléments, mais on les a donné implicitement. Exemple :

let rien = Translation (0.0,0.0)

Autre exemple :

let fait quelquechose t = match t with
     Rotation(a, (x, y)> a mod 360 != 0
   | Translation(x, y)> x != 0.0 || y != 0.0
   | SymCentrale(x, y)> true
   | Homothetie(x, (−, −))> r != 1.0

Les types récursifs

En Caml

type node = Node of int*node
| Vide

exemple d'éléments de "node" : - Vide - Node(0,Vide) - Node(1,Node(2,Node(3, Vide)))

Le type "node" est un type récursif. Comme les fonctions récursivent doivent avoir des cas de base, les types récursifs doivent avoir des constructeurs "de base" (non récursifs). On peut comparer des éléments avec "=". On peut aussi utiliser le filtrage.

Exemple :

let rec taille l = match l with
Vide -> 0
|Node(_,l)->1+taille l

La fonction taille sur les listes Caml suit le même schéma avec les abréviations Caml.

let rec taille l = match l with
[]->0
| a ::l->1+taille

Ceci explique la différence entre :: (constructeur) et @ (fonction récursive)

Exemple de type récursif, les arbres binaires :

type bin = Vide | Noeud of int*bin*bin

arbre naire :

type arbre n_aire = 
Vide|Node of int ∗ (arbre_naire list)(bancal)

Les structures

La récursivité terminale

Les exceptions

Aspects impératifs dans Caml

Les modules

Outils personnels