Programmation fonctionnelle (Ocaml) Notes de cours : Bases

publicité
Institut Galilée
AIR3 S1– Année 2013–2014
Programmation fonctionnelle (Ocaml)
Notes de cours : Bases
1
Documentation
Beaucoup d’informations, notamment le manuel et des livres en accès libre, sont disponible à partir de
http://caml.inria.fr. Regarder en particulier http://caml.inria.fr/pub/docs/manual-ocaml-4.
01/libref/Pervasives.html pour la liste des fonctions définies et utilisables de base.
2
Introduction
Concepts clés de la programmation fonctionnelle (et de Caml). Ces concepts se retrouvent dans
d’autres langages de programmation, y compris des langages non-fonctionnels qui incluent des aspects
fonctionnels (exemple : fonctions anonymes en Java ou JavaScript).
– Ordre supérieur (les fonctions sont des “citoyens de première classe”).
– Typage fort (notamment, différence int/float). Garanti l’exécution sans erreur (il peut y avoir
des exceptions).
– Inférence de type (pas besoin de donner le type des variables).
– Listes et récurrence.
– Style applicatif : on ne modifie pas un état de la mémoire, on calcule des nouvelles valeurs.
– Inspiré du λ-calcul (Church).
3
Une grosse calculette
Quelques phrases Caml et leurs résultats commentés.
# 1;;
Le # est le prompt de l’interprète. Une phrase Caml se termine par ;; qui dit à l’interprète d’évaluer ce
qu’on a tapé.
- : int = 1
Le type du résultat est déterminé automatiquement par Caml (inférence de type).
#
#
#
#
-
1.5;;
: float = 1.5
’a’;;
: char = ’a’
"azerty";;
: string = "azerty"
true;;
: bool = true
Quelques types de base.
La réponse de l’interprète est de la forme <nom> : <type> = <valeur>. Ici, rien, n’est nommé.
# 1+3;;
- : int = 4
Une grosse calculatrice. . .
1
# 1+3.9;;
Error: This expression has type float but an expression was expected of type int
# 1.;;
- : float = 1.
# 1.+3.9;;
Error: This expression has type float but an expression was expected of type int
# 1. +. 3.9;;
- : float = 4.9
Le typage est très strict. int et float sont deux types différents qui ne peuvent pas être mélangés (pas
de cast automatique). Les opérations usuelles sur les float sont suivies d’un point (+., *., /., . . . )
Noter la façon dont Caml indique les erreurs de type, c’est l’une de celles qu’on voit le plus souvent. . .
L’erreur de compilation This expression has type toto but an expression was
expected of type titi signifie en général :
– Si toto/titi sont int/float, qu’on a utilisé + au lieu de +. (ou variations).
– Si toto et titi sont différents (surtout si l’un des deux est une fonction ou un n-uplet),
qu’on a mal parenthésé l’expression et que Caml n’a pas les priorités qu’on croit.
– Si toto et titi sont le même type (défini par l’utilisateur) : Achievement unlocked. . .
# 1<4;;
- : bool = true
# float_of_int 4;;
- : float = 4.
# float_of_int 4.;;
Error: This expression has type float but an expression was expected of type int
Des opérations dont le type du résultat n’est pas celui des arguments.
# float_of_int;;
- : int -> float = <fun>
Les fonctions sont des “citoyens de première classe”. Une fonction peut donc être “évaluée” toute seule,
sans arguments. Le type d’une fonction comporte une flèche (->) ainsi que les types de départ et d’arrivée
(comme la notation mathématique f : A → B). La valeur d’une fonction est toujours la valeur abstraite
<fun>.
4
Tests et booléens
# if 2<4 then 4. else 5.;;
- : float = 4.
# if 2<4 then 4. else 5;;
Error: This expression has type int but an expression was expected of type float
# if 2<4 then 4 else 5;;
- : int = 4
# float_of_int (if 2<4 then 4 else 5);;
- : float = 4.
On peut faire des tests. Le type de chaque branche doit être le même, car c’est le type du résultat (on
peut le passer en argument à une autre fonction). Le if ... then ... else ... est donc similaire au
... ? ... : ... de C. Par exemple, on peut écrire en C 2<4 ? 4 : 5.
#
#
#
-
true && true;;
: bool = true
true || false;;
: bool = true
not true;;
: bool = false
Les opérations booléennes de base.
2
5
Nommer des objets
# let x=4;;
val x : int = 4
# x;;
- : int = 4
# let x=2;;
val x : int = 2
# x;;
- : int = 2
On peut nommer une valeur avec la construction let ... =, on peut réutiliser ce nom par la suite. Si on
redonne le même nom à une autre valeur, on détruit la première (c’est-à-dire qu’on crée une deuxième
variable de même nom, on ne modifie pas le contenu de la première).
# let
val f
# let
val f
f
:
f
:
x = x+1;;
int -> int = <fun>
= function x -> 2*x;;
int -> int = <fun>
Deux manières de définir des fonctions. On peut mettre les arguments à gauche du = ou on peut introduire
un argument à l’aide du mot clé function.
Noter le type des fonctions qui comporte une flèche. L’utilisation de function est proche de la
notation mathématique
f est la fonction x 7→ 2 × x.
#
#
-
function x -> 2*x;;
: int -> int = <fun>
(function x -> x+2) 4;;
: int = 6
On n’a pas besoin de nommer une fonction pour la définir. La fonction est une valeur comme une autre.
On parle alors de “fonction anonyme”. Bien évidemment, on ne peut utiliser une fonction anonyme que
immédiatement après sa définition puisque sa valeur est ensuite détruite.
6
Bizarreries syntaxiques
# let g x =
x+3 -12;;
val g : int -> int = <fun>
Une phrase Caml peut être sur plusieurs lignes. Seul le ;; termine la phrase.
#
#
-
(1+2)*3;;
: int = 9
begin 1+2 end *3;;
: int = 9
les parenthèses et le begin ...end sont strictement équivalent. L’usage est d’utiliser des parenthèses
dans une expression et des begin ...end pour grouper plusieurs expressions.
# let
let
val f
val g
f
g
:
:
x =
y =
int
int
x+1
2*y;;
-> int = <fun>
-> int = <fun>
Définitions simultanées de fonctions. Caml découvre tout seul où se termine la définition de f et les ;;
ne sont donc pas nécessaires. En particulier, dans un fichier de code isolé (et inclus dans l’interprète ou
compilé), on ne met généralement pas les ;;
3
7
Récurrence
# let rec fact n = if n=0 then 1 else n*(fact (n-1));;
val fact : int -> int = <fun>
# let fac n = if n=0 then 1 else n*(fac (n-1));;
Error: Unbound value fac
Une fonction récursive se définit avec un let rec au lieu du simple let.
L’erreur de compilation Unbound value toto signifie généralement :
– Soit qu’on a mis un let au lieu d’un let rec.
– Soit qu’on a fait une faute de frappe sur le nom d’une variable ou d’une fonction.
8
Couples, n-uplets
# 1,4;;
- : int * int = (1, 4)
# 1,"e",4.;;
- : int * string * float = (1, "e", 4.)
# let f (x,y) = x+y;;
val f : int * int -> int = <fun>
Un n-uplet regroupe plusieurs valeurs séparées par des virgules. Les valeurs peuvent être de types
différents. Le type du n-uplet est noté avec des *. Similaire au produit cartésien en mathématiques,
noté A × B.
# f 1,6;;
Error: This expression has type int but an expression was expected of type int * int
# f (1,6);;
- : int = 7
Attention au parenthésage implicite (priorité des opérations). L’application a une priorité plus élevée
que le couple. f 1,6 est donc interprété comme (f 1),6. Noter l’erreur “mauvais type” dans ces cas de
parenthèses oubliées.
# let add x y = x+y;;
val add : int -> int -> int = <fun>
Une fonction à deux arguments x et y peut être vue comme une fonction avec un seul argument, x, qui
renvoie une fonction (qui attend y et donne le résultat). Le passage d’une fonction sur les couples vers
cette notation s’appelle “curryfication”.
Mathématiquement, il y a un isomorphisme entre A × B → C (ensemble des fonctions à 2 arguments,
représentés par un produit cartésien) et A → (B → C) (ensemble des fonctions de A vers “les fonctions
de B vers C”).
Noter que le type devrait être int -> (int -> int). L’application (et la flèche) est associative à
droite, donc on peut se passer des parenthèses.
En pratique, une fonction dont le type comporte plusieurs flèches est une fonction à plusieurs
arguments.
# let add5 = add 5;;
val add5 : int -> int = <fun>
# add5 7;;
- : int = 12
La curryfication permet l’application partielle. Si on ne fournit qu’un argument à la fonction, on crée
une nouvelle fonction.
L’application partielle permet un style de programmation compact et lisible. Aussi, on curryfie
(presque) toujours les fonctions à plusieurs arguments et il est très rare d’avoir une fonction qui prend
un couple comme argument.
4
9
Ordre supérieur
# let carre_couple (x,y) = (x*x, y*y);;
val carre_couple : int * int -> int * int = <fun>
# let double_couple (x,y) = (2*x, 2*y);;
val double_couple : int * int -> int * int = <fun>
# let app_couple f (x,y) = (f x, f y);;
val app_couple : (’a -> ’b) -> ’a * ’a -> ’b * ’b = <fun>
Let fonctions carre_couple et double_couple ont la même structure : appliquer une fonction aux
membres d’un couple. Grâce à l’ordre supérieur, on peut définir une fonction plus générique qui attend
la fonction à appliquer.
Noter dans le type de app_couple :
– les ’a et ’b. On ne sait pas ici quel sera le type des éléments du couple. La fonction marche quelque
soit le type ’a et le type ’b. Mais il faut bien que les deux éléments du couple aient le même type
puisqu’ils sont tous les deux arguments de f.
On parle de polymorphisme. ’a et ’b sont appelées des variables de type. On les lit parfois α, β en
référence au λ-calcul.
– les parenthèses. ’a -> ’b -> ’a * ’a -> ’b * ’b serait une fonction à 3 arguments, respectivement de type ’a, ’b et ’a * ’a. Autrement dit, l’application (et la flèche) n’est pas associative à
gauche.
# let carre_double = app_couple (function x -> x*x);;
val carre_double : int * int -> int * int = <fun>
# let doublef_couple = app_couple (function x -> 2. *. x);;
val doublef_couple : float * float -> float * float = <fun>
# let convert_couple = app_couple (float_of_int);;
val convert_couple : int * int -> float * float = <fun>
L’application partielle permet de définir beaucoup de fonctions agissant sur les couples.
# let compose f g = function x -> f( g(x) );;
val compose : (’a -> ’b) -> (’c -> ’a) -> ’c -> ’b = <fun>
Le grand classique de l’ordre supérieur. . .
10
Reconnaissance de motif (“Pattern matching ”)
# let rec fac =
function
| 0 -> 1
| n -> fac (n-1) * n;;
val fac : int -> int = <fun>
# let rec fact n = if n=0 then 1 else n*(fact (n-1));;
val fact : int -> int = <fun>
La construction function peut aussi introduire une reconnaissance de motif. Il s’agit d’une généralisation
du switch case. En particulier, les valeurs reconnues ne sont pas forcément constantes et peuvent définir
des nouvelles variables (ici, n).
# let rec fibo =
function
| 0 -> 1
| 1 -> 1
| n -> (fibo (n-1)) + (fibo (n-2));;
val fibo : int -> int = <fun>
5
On peut avoir autant de cas que voulu. Pas besoin de break pour terminer un cas. Avoir plus de 2 cas
permet une écriture plus élégante que d’imbriquer des if then else.
# let rec chiffre n m =
(* TD 01, question 5 *)
match n with
| 0 -> m mod 10
| _ -> chiffre (n-1) (m/10);;
val chiffre : int -> int -> int = <fun>
La reconnaissance de motif avec function se fait nécessairement sur l’argument introduit par function,
donc sur le dernier argument de la fonction. Si on veut filtrer selon un autre argument, on utilise
match ... with. Noter que match n’introduit pas de nouvel argument, contrairement à function (n et
m sont définis comme arguments en étant placés à gauche du =).
On peut utiliser _ dans un motif comme cas par défaut (le default du C). On évite ainsi de créer
une nouvelle variable qui dans ce cas serait égale à n, ce qui compliquerait un peu la lecture.
11
Listes
Les langages fonctionnels supportent les listes de base comme “collection d’objets” (plutôt que les
tableaux des langages impératifs).
#
#
#
-
[1;4;8];;
: int list = [1; 4; 8]
[];;
: ’a list = []
3::[5;8;6;90];;
: int list = [3; 5; 8; 6; 90]
Les listes sont notées entre crochets, les éléments séparés par des points-virgules. La liste vide (“nil ”)
est notée []. Ajouter un élément en tête de liste se note :: (“cons”).
Le type liste est paramétré par un autre type (ici, int). De même qu’on ne mélange pas les entiers
et les chaı̂nes dans un tableau, tous les éléments d’une liste doivent être de même type.
# let rec length =
function
| [] -> 0
| tete::queue -> 1 + (length queue);;
val length : ’a list -> int = <fun>
Reconnaissance de motif sur les listes. Noter que le deuxième motif définit deux variables tete et queue
qui ne peuvent être utilisé que à droite de la flèche. Noter le squelette général de 90% des fonctions sur
les listes :
let rec f =
function
| [] -> ...
| a::r -> ...
(* cas liste vide *)
(* cas liste non vide *)
# let rec demie =
(* demie longueur d’une liste
function
| [] -> 0.
| a::[] -> 0.5
| a::b::queue -> (demie queue) +. 1.0;;
val demie : ’a list -> float = <fun>
*)
La reconnaissance de motif peut reconnaı̂tre n’importe quelle structure arbitrairement compliquée de
l’argument. Ici, on a un cas pour les listes vide, un cas pour les listes avec un seul élément et un cas pour
les listes avec au moins 2 éléments.
6
# let rec length2 =
function
| [] -> 0
| _::queue -> 1 + (length2 queue);;
val length : ’a list -> int = <fun>
# let rec demie2 =
function
| [] -> 0.
| _::[] -> 0.5
| _::_::queue -> (demie2 queue) +. 1.0;;
val demie2 : ’a list -> float = <fun>
Si on ne veut pas créer de variable, on peut utiliser _ qui reconnaı̂t tous les motifs. Ça permet d’optimiser
un peu le code mais surtout de le rendre plus lisible (on voit immédiatement que la valeur des éléments
n’est pas importante dans ces fonctions).
12
Pas d’état
# let x= 5;;
val x : int = 5
# let fx y = y+x;;
val fx : int -> int = <fun>
# fx 10;;
- : int = 15
# let x = 4;;
val x : int = 4
# fx 10;;
- : int = 15
Un deuxième let x = ne change pas l’ancienne valeur qui a été utilisée pour définir la fonction. On
crée une nouvelle variable (un nouveau nom) qui écrase l’ancien nom. Mais tout ce qui a été défini avec
l’ancien nom continue à l’utiliser.
13
Encapsulage
# let rec inverse f y n m =
(* TD 01, question 7 *)
if n>m
then m+1
else if (f n) = y
then n
else inverse f y (n+1) m;;
val inverse : (int -> ’a) -> ’a -> int -> int -> int = <fun>
# let inverse’ f y n m =
let rec go i =
if i > m
then m+1
else if (f i) = y
then i
else go (i+1)
in go n;;
val inverse’ : (int -> ’a) -> ’a -> int -> int -> int = <fun>
Quand peu d’arguments changent lors d’un appel récursif, on peut créer une fonction interne avec moins
de variables pour gérer la récurrence. La fonction externe n’est plus récursive.
7
Noter la notation let ... = ... in ... qui défini un nom à portée locale (uniquement dans l’expression qui suit le in).
14
Modules et pretty print
# Printf.printf "toto %d %f\n" 2 5.0;;
toto 2 5.000000
- : unit = ()
On dispose d’une fonction printf similaire à celle de C. En particulier, les formats sont les même.
Noter la notation pointée pour accéder à une fonction à l’intérieur d’un module (similaire à Java).
# open Printf;;
# printf "toto %d %f\n" 2 5.0;;
toto 2 5.000000
- : unit = ()
On peut “ouvrir” un module pour accéder à ses fonctions directement (sans notation pointée). C’est
généralement déconseillé car plusieurs fonctions similaires ont le même nom dans différents modules (par
exemple List.length et Array.length). C’est parfois utile pour des utilisations avancées (notamment,
programmation modulaire avec des modules utilisateurs et surtout foncteurs).
15
map
# let rec map f =
(* TD 01, questions 13 et 14 *)
function
| [] -> []
| a::r -> (f a)::(map f r);;
val map : (’a -> ’b) -> ’a list -> ’b list = <fun>
# List.map;;
- : (’a -> ’b) -> ’a list -> ’b list = <fun>
Une des fonctions les plus utiles du module List.
# let double x= 2*x;;
val double : int -> int = <fun>
# let double_l liste = List.map double liste;;
val double_l : int list -> int list = <fun>
# let double_l liste = (List.map double) liste;;
val double_l : int list -> int list = <fun>
# let double_l = (List.map double);;
val double_l : int list -> int list = <fun>
# let carre_l = List.map (function x -> x*x);;
val carre_l : int list -> int list = <fun>
Là encore, la curryfication et l’application partielle permettent de définir plein de fonctions qui agissent
sur les listes.
Noter :
– Quand le dernier argument d’une fonction est aussi le dernier argument de l’expression définie, on
peut le supprimer des deux côtés (η-conversion). Quand on défini (en mathématique) une fonction
g telle que g : x 7→ f (x) on n’a en fait (re)défini f et on peut directement dire g = f .
– Les fonctions anonymes sont particulièrement utiles avec la curryfication et l’application partielle.
On ne souhaite pas vraiment définir double (ou carre) qui ne sera pas utilisé plus tard. En passant
par une fonction anonyme, on évite de stocker (puis détruire) cette valeur. Attention, si la fonction
est compliquée, une fonction anonyme peut nuire à la compréhension.
8
Téléchargement