TP — Introduction à OCaml¶
Travaux pratiques · MP2I · 2021-2022 · OCaml
Cours : Typage en OCaml · Récursivité
Principes généraux du langage¶
OCaml est un langage fonctionnel. Les fonctions jouent un rôle primordial
par opposition aux objets dans un langage objets ou aux instructions dans un
langage impératif (c'est un peu plus compliqué que cela, on y reviendra dans
le chapitre sur les paradigmes).
OCaml est un langage fortement typé dans lequel les types, même s'ils sont
omis dans le programme car inférés par le compilateur, jouent un rôle très
important.
Quand on évalue dans l'interprète n'importe quelle valeur OCaml, elle a un
type et OCaml répond comme ici type = valeur.
3
- : int = 3
Les types de base¶
Entiers¶
Comme on vient de le voir, le type des entiers relatifs est int.
Contrairement à des langages comme Python, les entiers en OCaml ont une
longueur fixe en mémoire. Sur ce serveur, ils sont compris entre \(-2^{62}\) et
\(2^{62}-1\). Pour les manipuler, on utilise les opérations élémentaires +,
-, * et /. Cette dernière division correspond au quotient dans la
division euclidienne, on accède au reste avec mod : 12 mod 5 s'évalue
ainsi sur 2.
Les flottants¶
Le type float correspond aux nombres à virgule flottante. Il s'agit d'une
représentation des nombres réels qui, bien que ne permettant d'en manipuler
qu'un nombre fini, permet tout de même de faire des calculs numériques. Les
opérations qu'on vient de voir sur les entiers ne s'appliquent pas sur des
float. Avec OCaml les opérations ont un unique type pour les opérandes et
la valeur calculée. Il y a donc d'autres opérateurs pour les flottants, ce
sont les mêmes que précédemment mais avec un . à la fin qui rappelle la
notation de la virgule : +., -., *. et /.. De plus, on a
l'exponentiation a ** b pour calculer \(a^b\), des fonctions mathématiques
usuelles comme sqrt, cos, sin, …
On peut passer d'un entier à un flottant à l'aide de la fonction
float_of_int, ainsi float_of_int 3 correspond au flottant 3.0. On
convertit dans l'autre sens avec int_of_float mais cette opération réalise
une troncature.
Les chaînes de caractères et les caractères¶
Les caractères en OCaml sont représentés avec des apostrophes 'a' et les
chaînes avec des guillemets "bonjour". Le type des caractères est char et
celui des chaînes string. On verra quand ce sera utile la manipulation des
chaînes.
Unit¶
Si tout a un type en OCaml, comment faire avec des expressions qui n'ont
aucune valeur ? Par exemple une instruction d'affichage, comme ici pour faire
un saut de ligne. On voit que la valeur () est celle renvoyée et également
celle passée en paramètre, c'est l'unique valeur du type unit
correspondant.
print_newline ()
- : unit = ()
Une expression qui n'a aucune valeur est appelée communément une
instruction en référence aux langages de programmation impératifs. On peut
utiliser l'opérateur ; pour enchaîner des instructions :
instr1 ; ... ; instrn ; expr, la valeur sera alors la valeur de expr. Si
jamais une des instructions n'en est pas une et a une valeur différente de
(), OCaml produira un avertissement.
Les booléens et les expressions logiques¶
Comme dans les autres langages, les expressions logiques sont très
importantes car elles permettent d'exprimer des conditions pour les
instructions conditionnelles ou les boucles. En OCaml, il s'agit du type
bool qui contient deux éléments true et false. On utilise très rarement
ces valeurs directement dans un programme. En effet, elles sont produites par
les relations de comparaison usuelles comme =, <, <=, > et >=.
Contrairement aux opérations arithmétiques vues précédemment, ces
comparaisons fonctionnent pour tous les types. On peut ainsi écrire 3 = 2
(qui s'évalue à false) ou 5.1 = 5.1 (qui s'évalue à true).
Les opérations logiques usuelles sont :
notpour la négation :not bs'évalue àtruesi et seulement sibvautfalse;&&pour la conjonction :b1 && b2s'évalue àtruesi et seulement sib1etb2valenttrue;||pour la disjonction :b1 || b2s'évalue àtruesi et seulement sib1oub2valenttrue.
Une des manières les plus fréquentes d'utiliser les expressions booléennes est l'expression conditionnelle :
if condition
then expr1
else expr2
qui s'évalue en expr1 si la condition vaut true et expr2 sinon.
ATTENTION le type de expr1 doit être celui de expr2 et c'est alors le
type de l'expression conditionnelle. Ainsi on ne peut pas écrire
if b then 2 else 3.2
Quand la branche else ne comporte que la valeur () du type unit, ce qui
correspond au cas où on ne veut rien faire dans le cas où la condition n'est
pas vérifiée, on peut l'omettre. Ainsi,
if condition then expr else ()
peut s'écrire plus simplement
if condition then expr
ATTENTION le type de expr doit impérativement être unit.
Quand on enchaîne plusieurs instructions conditionnelles, ce n'est pas le plus clair :
if b1 then if b2 then e1 else if b3 then e2 else e3 else e4
On pourra alors rajouter des parenthèses pour clarifier :
if b1 then (if b2 then e1 else (if b3 then e2 else e3)) else e4
Il est aussi possible d'utiliser begin/end à la place de (/) :
if b1
then begin
if b2
then e1
else begin if b3 then e2 else e3 end
end else e4
Les variables¶
Les variables ont un statut très différent en OCaml par rapport aux autres
langages, ce sont de simples alias pour des valeurs définies par une
expression. On fait la distinction entre les variables définies globalement
et celles définies localement dans l'évaluation d'une expression.
Variable globale¶
On écrit, par exemple,
let x = 3
pour définir une variable x représentant la valeur 3 dans la suite du
programme. Ce nom x peut alors être utilisé partout où on aurait pu écrire
3. La variable a ainsi une valeur et un type unique. Ils ne pourront jamais
changer. Tout ce qu'on peut faire, c'est définir une nouvelle variable appelée
x, mais alors ça ne changera aucune des valeurs précédemment calculées avec
l'ancien x (ce qui est assez rassurant en fait).
On remarque que lorsqu'on définit une variable, OCaml répond
val variable : type = valeur.
Ici on va avoir une autre variable de type int contenant la valeur 6 :
let y = x + 3
si on écrit ensuite
let x = 5
cela ne changera pas la valeur de y comme on peut le constater ici :
let x = 3
let y = x + 3
let x = 5
val x : int = 3
val y : int = 6
val x : int = 5
y
- : int = 6
Variable locale¶
On peut définir une variable utilisée uniquement dans l'évaluation d'une expression.
let var = valeur in expr
La variable n'est alors accessible que pour cette expression.
Les fonctions¶
OCaml est un langage fonctionnel, ainsi les fonctions sont essentielles et
sont des valeurs comme les autres. Pour définir une fonction on utilise une
syntaxe proche de la syntaxe de définition des variables sauf qu'on rajoute
un certain nombre de paramètres. Par exemple, la fonction suivante calcule le
produit de ses trois arguments :
let produit3 x y z = x * y * z
val produit3 : int -> int -> int -> int = <fun>
On voit que OCaml devine tout seul le type des arguments et le type de la
valeur de retour. Le type de la fonction lui-même est un peu étrange avec
plusieurs arguments. On y reviendra dans la suite mais pour le moment, on se
contentera de dire qu'il est de la forme
type1 -> type2 -> ... -> typen -> typer où type1 est le type du premier
argument, type2 celui du deuxième, et ainsi de suite jusqu'à typer le
type des valeurs de retour de la fonction.
Pour appliquer une fonction, il suffit d'écrire fonction arg1 ... argn :
pas de parenthèses, pas de virgules, juste des espaces.
produit3 1 2 3
- : int = 6
Question 1
Écrire une fonction carre qui prend un entier x en paramètre et
renvoie son carré.
Tests
assert(carre 2 = 4)
Corrigé
let carre x = x * x
Question 2
Écrire une fonction multadd qui prend trois entiers x, y et z en
paramètres et renvoie x * (y + z).
Tests
assert(multadd 2 3 4 = 14)
Corrigé
let multadd x y z = x * (y + z)
Types génériques¶
Si on définit une fonction comme let identite x = x on voit bien qu'aucune
contrainte ne peut être imposée arbitrairement sur le type de x. En fait,
cette fonction pourra s'appliquer sur une valeur de n'importe quel type, la
seule certitude est que la valeur de retour sera du même type que celui de
l'entrée. Si on évalue cette fonction, on voit que OCaml répond
val identite : 'a -> 'a = <fun>
Ce 'a signifie n'importe quel type. Comme prévu, on voit que c'est le même
'a qu'on retrouve en entrée et en sortie.
Essayez de deviner les types des fonctions suivantes avant d'évaluer la cellule :
let f x y = y
let g x y = 2+x
let h x y z = x *. z
Réponse
val f : 'a -> 'b -> 'b = <fun>
val g : int -> 'a -> int = <fun>
val h : float -> 'a -> float -> float = <fun>
Question 3
Écrire une fonction max qui prend deux éléments d'un même type et
renvoie le plus grand. On rappelle que les comparaisons ne sont pas
typées.
Tests
assert (max 0 1 = 1);
assert (max "a" "b" = "b")
Corrigé
let max a b = if a > b then a else b
Question 4
Écrire une fonction mediane3 qui prend trois éléments d'un même type,
tous distincts, et renvoie l'élément au milieu dans l'ordre. Si on lui
passe x < y < z elle renvoie y.
Tests
assert(mediane3 1 2 3 = 2);
assert(mediane3 2 1 3 = 2);
assert(mediane3 1 3 2 = 2);
assert(mediane3 3 2 1 = 2);
assert(mediane3 2 3 1 = 2);
assert(mediane3 3 1 2 = 2);
assert(mediane3 'a' 'b' 'c' = 'b')
Corrigé
let mediane3 x y z =
if x > y then begin
if y > z then y
else begin if x > z then z else x end
end
else begin if x > z then x
else begin if y > z then z else y end
end
Les fonctions comme des valeurs de première classe¶
Les fonctions en OCaml sont manipulables directement comme n'importe quelle
valeur. La fonction suivante prend une fonction f et une valeur x en
paramètre et applique f sur x. Son type est un peu compliqué mais si on
décompose on comprend que x a un type quelconque 'a et comme on applique
f dessus, il faut que f prenne des éléments de type 'a en argument.
Aucune contrainte sur son type de retour, ainsi on indique que c'est un
nouveau type générique 'b. Donc le type de f est 'a -> 'b et le type
global de applique est ('a -> 'b) -> 'a -> 'b.
Attention Les parenthèses sont nécessaires. Le type
'a -> 'b -> 'a -> 'b serait celui d'une fonction à trois arguments dont le
premier et le troisième sont de type 'a et le second de type 'b.
let applique f x = f x
val applique : ('a -> 'b) -> 'a -> 'b = <fun>
Question 5
Compléter la fonction compose :
let compose f g x =
(* Compose prend deux fonctions f et g et un élément x et calcule
l'image par x de la composée de f et de g, donc f(g(x)) en
notation mathématique usuelle *)
Tests
(* Ce test vérifie que compose est du bon type *)
let _ = ( compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b ) in
(* Test sur une composition, on verra la syntaxe utilisée dans la suite *)
assert (compose ((+) 2) (( * ) 3) 4 = 14)
Corrigé
let compose f g x =
f (g x)
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>
Fonctions anonymes¶
Les fonctions sont tellement importantes en OCaml qu'il est parfois
pratique d'en écrire sans avoir à les définir en leur donnant un nom. On
parle alors de fonctions anonymes, et cela correspond à la notation
mathématique usuelle \(x \mapsto x^2\).
Pour définir une fonction en tant que valeur ainsi, il suffit d'écrire
fun x -> x*x
Une telle valeur peut alors être stockée dans une variable comme n'importe quelle autre valeur :
let carre = fun x -> x*x
va alors être une définition complètement équivalente à la précédente :
let carre x = x*x
En fait, cette dernière syntaxe est automatiquement transformée en la syntaxe utilisant une fonction anonyme.
Ainsi, en utilisant la fonction compose précédente, on peut écrire
compose (fun x -> x+3) (fun x -> x*4) 5
qui s'évaluera en \(4 \times 5 + 3 = 23\).
Pour des fonctions prenant plusieurs arguments, on utilise la syntaxe
fun arg1 ... argn -> valeur
Par exemple, la fonction suivante fun x y -> x+y réalise l'addition.
Curryfication¶
Nous allons revenir sur la notation des types de fonctions à plusieurs
arguments ici. En fait, les fonctions n'ont qu'un seul argument en OCaml et
sont nécessairement d'un type a -> b. Afin de définir des fonctions de
plusieurs arguments, on définit des fonctions qui renvoient des fonctions.
Ainsi lorsqu'on écrit fun x y -> x+y en fait, c'est retraduit par OCaml
en fun x -> ( fun y -> x+y ). Si on sait que x est de type int, alors
fun y -> x+y a le type int -> int. Donc fun x -> (fun y -> x+y) a le
type int -> (int -> int).
Par défaut, on considère que les parenthèses autour des -> sont toujours à
droite, et on se permet ainsi d'écrire int -> int -> int. On retrouve bien
le type de la fonction fun x y -> x+y vu précédemment.
Attention le type (int -> int) -> int est lui bien différent car il
correspond au type d'une fonction ayant un argument du type fonctionnel
int -> int et qui renvoie un entier. C'est le cas de la fonction suivante
d'évaluation en 1 :
let eval1 f = f 1
Le fait que les fonctions à plusieurs arguments soient des fonctions de
fonctions permet de pouvoir les appliquer partiellement. En effet, quand on
applique f x1 ... xn en fait, on évalue (... ((f x1) x2) ...) xn. Mais
rien ne nous oblige à donner tous les arguments. Par exemple si on a défini
let ajoute x y = x+y
l'évaluation ajoute 3 est une fonction équivalente à fun y -> 3+y. On
parle, pour ce mécanisme d'évaluation partielle, de curryfication et on dit
que ajoute 3 est une curryfiée de ajoute. Cela fait référence à Haskell
Curry, précurseur de la programmation fonctionnelle.
La récursivité¶
Jusqu'ici toutes les fonctions ou expressions définies s'évaluent assez directement. On est loin de la richesse qu'on connaît dans d'autres langages. La manière usuelle de retrouver cette expressivité dans un langage fonctionnel est de passer par la récursivité. On dit qu'une fonction ou une expression est récursive quand elle fait référence à elle-même. Le cas des expressions récursives est marginal, on ne considère ici que des fonctions.
Naturellement, comme on y fait référence par son nom, cela ne peut concerner
que des valeurs définies par let. Pour définir une fonction récursive, on
utilise let rec plutôt que let.
On peut ainsi définir la factorielle mathématique usuelle :
let rec fact n =
if n = 0
then 1
else n * fact (n-1)
val fact : int -> int = <fun>
fact 5
- : int = 120
Il est possible de demander à OCaml de retracer l'évaluation de fact 5
pour faire apparaître les appels. On voit qu'on a besoin d'empiler les appels
sur des arguments de plus en plus petits jusqu'à aboutir au cas de base :
fact 0.
#trace fact;;
fact 5
fact is now traced.
fact <-- 5
fact <-- 4
fact <-- 3
fact <-- 2
fact <-- 1
fact <-- 0
fact --> 1
fact --> 1
fact --> 2
fact --> 6
fact --> 24
fact --> 120
- : int = 120
Question 6
Écrire une fonction pow2 telle que pow2 n renvoie \(2^n\).
Tests
assert(pow2 0 = 1);
assert(pow2 1 = 2);
assert(pow2 4 = 16);
assert(pow2 8 = 256)
Corrigé
let rec pow2 n = if n = 0 then 1 else 2 * pow2 (n-1)
Question 7
Écrire une fonction somme_carres telle que somme_carres n renvoie la
valeur de la somme \(1^2 + 2^2 + \dots + n^2\).
Tests
assert(somme_carres 10 = 385);
assert(somme_carres 0 = 0);
assert(somme_carres 1 = 1)
Corrigé
let rec somme_carres n =
if n = 0
then 0
else n*n + somme_carres (n-1)
On appelle suite de Fibonacci, la suite \((F_n)\) définie par \(F_0 = 0\), \(F_1 = 1\) et pour \(n\) entier, \(F_{n+2} = F_{n+1} + F_n\).
Question 8
Écrire une fonction fibo telle que fibo n renvoie \(F_n\).
Tests
assert (fibo 8 = 21)
Corrigé
let rec fibo n =
Printf.printf "%d\n" n;
if n = 0 then 0
else if n = 1 then 1
else fibo (n-1) + fibo (n-2)
Question 9
Ce calcul prend beaucoup de temps, pourquoi ?
fibo 4; flush stdout
Expliquez pourquoi le calcul précédent prend du temps ici.
Affichage obtenu avec le corrigé précédent
4
2
0
1
3
1
2
0
1
Tuples¶
Il est possible de considérer, comme en mathématiques, un produit cartésien de différents types. On parlera plutôt de tuple dans le contexte de l'informatique.
Pour construire un tuple il suffit d'écrire plusieurs valeurs ainsi :
(3, 2.2, "bonjour")
- : int * float * string = (3, 2.2, "bonjour")
On remarque que le type est alors de la forme type1 * type2 * ... * typen.
Si la création d'un tuple est facile, son utilisation nécessite de casser le
tuple pour en récupérer les différentes composantes. Pour cela, on va
utiliser une fonctionnalité du let : on peut écrire let motif = valeur où
motif est lié à la structure de valeur.
Commençons par l'exemple le plus simple :
let x, y = (1, "a")
est équivalent à la définition :
let x = 1
let y = "a"
mis à part que ces deux définitions sont simultanées.
On peut tout à fait mettre des tuples dans des tuples.
(3, (2.2, "bonjour"))
- : int * (float * string) = (3, (2.2, "bonjour"))
Les parenthèses dans le type sont alors très importantes. Pour utiliser une telle valeur, on peut employer une syntaxe similaire à la précédente :
let a, (b, c) = (3, (2.2, "bonjour"))
Ici, il faut remarquer qu'il est possible de déconstruire ce tuple de
plusieurs manières. On peut écrire comme précédemment pour avoir chaque
composante, ou let a, t = ... pour que t contienne le couple
(2.2, "bonjour").
On peut se servir de cela pour réaliser une sorte d'échange de variables :
let x, y = y, x in ...
En fait, on définit de nouvelles variables, mais en pratique, cela correspond
à l'échange usuel. On peut alors simplifier le code mediane3 :
let mediane3 x y z =
(* range x < y *)
let x, y = if x > y then y, x else x, y in
if z < x then x
else if z > y then y else z
Question 10
Question difficile - revenez dessus plus tard si vous n'y arrivez pas tout de suite.
Écrire une fonction mediane5 qui prend cinq éléments d'un même type,
tous distincts, et renvoie l'élément au milieu dans l'ordre. Si on lui
passe x < y < z < t < u elle renvoie z. On pourra utiliser une
fonction min sur le modèle de la fonction max.
Tests
(* Ici il y a 120 tests si on veut tout vérifier... *)
assert(mediane5 1 2 3 4 5 = 3); assert(mediane5 1 2 3 5 4 = 3); assert(mediane5 1 2 4 3 5 = 3);
assert(mediane5 1 2 4 5 3 = 3); assert(mediane5 1 2 5 3 4 = 3); assert(mediane5 1 2 5 4 3 = 3);
assert(mediane5 1 3 2 4 5 = 3); assert(mediane5 1 3 2 5 4 = 3); assert(mediane5 1 3 4 2 5 = 3);
assert(mediane5 1 3 4 5 2 = 3); assert(mediane5 1 3 5 2 4 = 3); assert(mediane5 1 3 5 4 2 = 3);
assert(mediane5 1 4 2 3 5 = 3); assert(mediane5 1 4 2 5 3 = 3); assert(mediane5 1 4 3 2 5 = 3);
assert(mediane5 1 4 3 5 2 = 3); assert(mediane5 1 4 5 2 3 = 3); assert(mediane5 1 4 5 3 2 = 3);
assert(mediane5 1 5 2 3 4 = 3); assert(mediane5 1 5 2 4 3 = 3); assert(mediane5 1 5 3 2 4 = 3);
assert(mediane5 1 5 3 4 2 = 3); assert(mediane5 1 5 4 2 3 = 3); assert(mediane5 1 5 4 3 2 = 3);
assert(mediane5 2 1 3 4 5 = 3); assert(mediane5 2 1 3 5 4 = 3); assert(mediane5 2 1 4 3 5 = 3);
assert(mediane5 2 1 4 5 3 = 3); assert(mediane5 2 1 5 3 4 = 3); assert(mediane5 2 1 5 4 3 = 3);
assert(mediane5 2 3 1 4 5 = 3); assert(mediane5 2 3 1 5 4 = 3); assert(mediane5 2 3 4 1 5 = 3);
assert(mediane5 2 3 4 5 1 = 3); assert(mediane5 2 3 5 1 4 = 3); assert(mediane5 2 3 5 4 1 = 3);
assert(mediane5 2 4 1 3 5 = 3); assert(mediane5 2 4 1 5 3 = 3); assert(mediane5 2 4 3 1 5 = 3);
assert(mediane5 2 4 3 5 1 = 3); assert(mediane5 2 4 5 1 3 = 3); assert(mediane5 2 4 5 3 1 = 3);
assert(mediane5 2 5 1 3 4 = 3); assert(mediane5 2 5 1 4 3 = 3); assert(mediane5 2 5 3 1 4 = 3);
assert(mediane5 2 5 3 4 1 = 3); assert(mediane5 2 5 4 1 3 = 3); assert(mediane5 2 5 4 3 1 = 3);
assert(mediane5 3 1 2 4 5 = 3); assert(mediane5 3 1 2 5 4 = 3); assert(mediane5 3 1 4 2 5 = 3);
assert(mediane5 3 1 4 5 2 = 3); assert(mediane5 3 1 5 2 4 = 3); assert(mediane5 3 1 5 4 2 = 3);
assert(mediane5 3 2 1 4 5 = 3); assert(mediane5 3 2 1 5 4 = 3); assert(mediane5 3 2 4 1 5 = 3);
assert(mediane5 3 2 4 5 1 = 3); assert(mediane5 3 2 5 1 4 = 3); assert(mediane5 3 2 5 4 1 = 3);
assert(mediane5 3 4 1 2 5 = 3); assert(mediane5 3 4 1 5 2 = 3); assert(mediane5 3 4 2 1 5 = 3);
assert(mediane5 3 4 2 5 1 = 3); assert(mediane5 3 4 5 1 2 = 3); assert(mediane5 3 4 5 2 1 = 3);
assert(mediane5 3 5 1 2 4 = 3); assert(mediane5 3 5 1 4 2 = 3); assert(mediane5 3 5 2 1 4 = 3);
assert(mediane5 3 5 2 4 1 = 3); assert(mediane5 3 5 4 1 2 = 3); assert(mediane5 3 5 4 2 1 = 3);
assert(mediane5 4 1 2 3 5 = 3); assert(mediane5 4 1 2 5 3 = 3); assert(mediane5 4 1 3 2 5 = 3);
assert(mediane5 4 1 3 5 2 = 3); assert(mediane5 4 1 5 2 3 = 3); assert(mediane5 4 1 5 3 2 = 3);
assert(mediane5 4 2 1 3 5 = 3); assert(mediane5 4 2 1 5 3 = 3); assert(mediane5 4 2 3 1 5 = 3);
assert(mediane5 4 2 3 5 1 = 3); assert(mediane5 4 2 5 1 3 = 3);
assert(mediane5 1 3 5 7 9 = 5); assert(mediane5 10 13 11 12 14 = 12)
Corrigé
let min a b = if a < b then a else b
let mediane5 a b c d e =
let a, b = if a < b then a, b else b, a in
let d, e = if d < e then d, e else e, d in
let a, b, d, e = if a < d then a, b, d, e else d, e, a, b in
if c > b
then begin
if b < d
then min c d (* car a < b < d < e et b < c donc soit c < d < e, d < e < c ou d < c < e *)
else min b e
end else begin
if c > d then min c e else min b d
end
À l'aide de couples, on peut calculer plus rapidement \(F_n\). En effet, on pose \(C_n = (F_n, F_{n+1})\) avec \(C_0 = (0, 1)\).
Question 11
Écrire une fonction fiboC telle que fiboC n renvoie le couple \(C_n\).
Tests
assert (fiboC 8 = (21, 34))
Corrigé
let rec fiboC n =
if n = 0 then (0, 1)
else let a, b = fiboC (n-1) in b, a+b
Question 12
Cet appel prend-il autant de temps qu'avec fibo ? Expliquez.
fiboC 40
- : int * int = (102334155, 165580141)
Filtrage¶
Une des grandes forces du langage OCaml est de permettre de faire du
filtrage de valeur. Cela consiste, étant donnée une valeur, à appliquer
différentes expressions selon la forme de cette valeur. Pour cette notion de
forme on introduit la notion de motif qui sont des expressions sous forme
évaluée présentant des variables ou des constantes. Par exemple (0, x) mais
pas 1+1 (qui est 2 sous forme évaluée).
La syntaxe est alors
match valeur with
| motif1 -> expr1
| ...
| motifn -> exprn
Un premier exemple en reprenant fibo :
let rec fibo n =
match n with
| 0 -> 0
| 1 -> 1
| n -> fibo (n-1) + fibo (n-2)
Ici le dernier motif va définir une nouvelle variable n qui prendra la
valeur de la précédente. Comme cela n'apporte rien, on remplace en général le
dernier motif par le motif joker _ : | _ -> ....
La force du filtrage c'est qu'on peut déconstruire des types plus complexes.
Pour le moment, nous n'avons vu que les tuples. On peut ainsi écrire la
fonction suivante qui renvoie true si un triplet contient la valeur 0 :
let f t = match t with
| (0, _, _) -> true
| (_, 0, _) -> true
| (_, _, 0) -> true
| _ -> false
Les motifs sont parcourus du haut vers le bas, ainsi la fonction suivante
appliquée au couple (x,y) renvoie y si x est nul ou x sinon.
let f c = match c with
| (0, y) -> y
| (x, _) -> x
Si on inverse, elle renverra toujours x car la seconde ligne ne sera jamais
atteinte.
let f c = match c with
| (x, _) -> x
| (0, y) -> y
On peut avoir une structure riche dans les motifs, ainsi | (x, (_, 0)) ->
va filtrer les couples dont la seconde composante est un couple de deuxième
composante nulle en liant à la variable x la valeur de la première
composante. Par exemple, cela filtrera (1, (2,0)) en liant 1 à la
variable x.
En fait, on a déjà vu cela en déconstruisant les tuples et on peut se dire
que let motif = expr in suite est équivalent à
match expr with | motif -> suite.
Suite de Syracuse¶
Étant donné \(a \in \N^*\), on appelle suite de Syracuse de premier terme \(a\), la suite \((u_n)_{n\in\N}\) définie par \(u_0 = a\) et pour \(n \in \N\) :
Rappel : pour déterminer si un nombre a est pair on peut utiliser le modulo
et tester a mod 2 = 0.
Question 13
Écrire une fonction suivant telle que suivant \(u_n\) renvoie
\(u_{n+1}\) selon la définition précédente.
Tests
assert (suivant 14 = 7);
assert (suivant 7 = 22)
Corrigé
let suivant u = if u mod 2 = 0 then u / 2 else 3 * u + 1
Question 14
Écrire une fonction syracuse telle que syracuse a n renvoie le
\(n\)-ième terme de la suite de Syracuse de premier terme \(a\).
Tests
assert (syracuse 3 5 = 4);
assert (syracuse 1 0 = 1);
assert (syracuse 1 1 = 4);
assert (syracuse 1 2 = 2)
Corrigé
let rec syracuse a n =
if n = 0
then a
else suivant (syracuse a (n-1))
En évaluant les termes de la suite, on constate qu'elle semble devenir périodique à partir d'un certain rang. Par exemple, en partant de \(a = 14\) :
Quelle que soit la valeur de \(u_0\), on retombe sur ce cycle. (Demandez une preuve à votre professeur de mathématiques à 12h05.)
On appelle temps de vol pour \(a\), le plus petit \(n\) tel que \(u_n = 1\) et altitude maximale pour \(a\) la plus grande valeur prise par la suite. Ici, le temps de vol pour 14 est 17 et son altitude maximale est 52.
Question 15
Écrire une fonction temps_de_vol telle que temps_de_vol a renvoie le
temps de vol pour a.
On pourra faire le lien entre le temps de vol de a et celui de
suivant a.
Tests
assert (temps_de_vol 1 = 0);
assert (temps_de_vol 14 = 17)
Corrigé
let rec temps_de_vol a =
if a = 1 then 0 else 1 + temps_de_vol (suivant a)
Question 16
Écrire une fonction altitude_maximale telle que altitude_maximale a
renvoie l'altitude maximale de a.
On pourra faire le lien entre altitude_maximale (suivant a) et celle de
a.
Tests
assert (altitude_maximale 1 = 4);
assert (altitude_maximale 2 = 4);
assert (altitude_maximale 14 = 52)
Corrigé
let rec altitude_maximale a =
if a = 1 then 4 else max a (altitude_maximale (suivant a))
Question 17
Écrire une fonction plus_long_temps_de_vol telle que
plus_long_temps_de_vol n renvoie le plus grand temps de vol parmi les
entiers compris entre 1 et n (inclus).
Tests
assert (plus_long_temps_de_vol 1 = 0);
assert (plus_long_temps_de_vol 12 = 19);
assert (plus_long_temps_de_vol 50 = 111)
Corrigé
let rec plus_long_temps_de_vol n =
if n = 1 then 0 else max (temps_de_vol n) (plus_long_temps_de_vol (n-1))
Facultatif - Entiers de Church¶
Juste avec les fonctions et les applications, on peut définir des expressions qui se comportent comme les entiers naturels. Ce sont les entiers de Church.
On pose ainsi :
let zero = fun f -> fun x -> x
let un = fun f -> fun x -> f x
let deux = fun f -> fun x -> f (f x)
let trois = fun f -> fun x -> f (f (f x))
Le type de ces entiers est donc ('a -> 'a) -> 'a -> 'a.
Question 18
Compléter la fonction int_of_church :
let int_of_church c =
(* renvoie sous forme de int l'entier représenté par c *)
Tests
let zero = fun f -> fun x -> x in
let un = fun f -> fun x -> f x in
let deux = fun f -> fun x -> f (f x) in
let trois = fun f -> fun x -> f (f (f x)) in
assert (int_of_church zero = 0);
assert (int_of_church un = 1);
assert (int_of_church deux = 2);
assert (int_of_church trois = 3)
Corrigé
let int_of_church c =
c (fun n -> n+1) 0
Question 19
Compléter la fonction church_of_int :
let rec church_of_int n =
(* renvoie l'entier de Church correspondant à l'int n *)
Tests
for i = 0 to 10 do
assert (int_of_church (church_of_int i) = i)
done
Corrigé
let rec church_of_int n =
if n = 0 then fun f -> fun x -> x
else fun f -> fun x -> f ((church_of_int (n-1)) f x)
Question 20
Compléter la fonction successeur :
let successeur a =
(* prend un entier de Church a et renvoie l'entier correspondant à
son successeur. Bien sûr on ne passera pas par les entiers ! *)
Tests
for i = 0 to 10 do
assert (int_of_church (successeur (church_of_int i)) = i+1)
done
Corrigé
let successeur a =
fun f -> fun x -> a f (f x)
Question 21
Compléter la fonction ajoute :
let rec ajoute a b =
(* Prend deux entiers de Church a et b et renvoie l'entier de Church
correspondant à leur somme. Bien sûr on ne passera pas par les
entiers ! *)
Tests
assert (int_of_church (ajoute (church_of_int 4) (church_of_int 5)) = 9)
Corrigé
let rec ajoute a b =
fun f -> fun x -> a f (b f x)
Question 22
Compléter la fonction multiplie :
let rec multiplie a b =
(* Prend deux entiers de Church a et b et renvoie l'entier de Church
correspondant à leur produit. Bien sûr on ne passera pas par les
entiers ! *)
Tests
assert (int_of_church (multiplie (church_of_int 4) (church_of_int 5)) = 20)
Corrigé
let rec multiplie a b =
fun f -> fun x -> a (fun x -> b f x) x