Structure des programmes OCaml
Maintenant nous allons passer un peu de temps à observer la structure
d'ensemble de quelque vrais programmes en OCaml. Je veux vous parler des
définitions locales et globales, quand utiliser ;;
ou ;
, les
modules, les fonctions imbriquées, les références. Pour cela nous allons
passer sur plein de concepts d'OCaml qui ne vous diront rien car nous ne
les avons pas encore évoqués. Ne vous embarrassez pas des détails pour
le moment. Occupez-vous uniquement de la forme générale des programmes,
et aux fonctionnalités mises en avant.
« Variables » locales (plus exactement expressions locales)
Prenons la fonction average
écrite en C, et ajoutons lui une variable
locale. (à comparer avec la première version que nous avions)
double
average (double a, double b)
{
double sum = a + b;
return sum / 2;
}
Faisons la même chose avec notre version OCaml :
# let average a b =
let sum = a +. b in
sum /. 2.0;;
val average : float -> float -> float = <fun>
La formule standard let nom = expression in
sert à définir une
expression locale nommée, et nom
peut ensuite être utilisé dans la
fonction à la place de expression
jusqu'au ;;
qui marque la fin du
bloc de code. Remarquez que nous n'avons pas changé l'indentation après
in
. Considérez let ... in
comme s'il s'agissait d'une instruction.
Maintenant, traduire les variables locales en C par ces expressions
locales est une supercherie. En fait ils sont un peu différents. La
variable C sum
a un emplacement réservé dans la pile. Vous pouvez
changer la valeur associée à sum
plus loin dans la fonction si vous
voulez, ou même prendre l'adresse de sum
. Ce n'est PAS possible dans
la version OCaml. Dans la version OCaml, sum
est juste une abréviation
pour l'expression a +. b
. Il est impossible d'assigner ou de changer
la valeur de sum
. (Nous verrons comment avoir de vraies variables un
peu plus loin).
Voyons un autre exemple qui devrait clarifier les choses. Les deux bouts de code suivants devraient retourner la même valeur (à savoir (a+b) + (a+b)2):
# let f a b =
(a +. b) +. (a +. b) ** 2.;;
val f : float -> float -> float = <fun>
# let f a b =
let x = a +. b in
x +. x ** 2.;;
val f : float -> float -> float = <fun>
Il se peut que la seconde version soit plus rapide (bien que la plupart
des compilateurs devraient être capable de faire cette étape
d'"élimination de sous-expressions communes" à votre place), et en tout
cas elle est plus lisible. Dans le second exemple, x
est une
abréviation pour l'expression a +. b
.
« Variables » globales (plus précisément expressions globales)
Vous pouvez aussi donner des noms à des choses dans l'environnement global, et comme pour les "variables" locales ci-dessus, ce ne sont pas vraiment des variables, mais des abbréviations pour ces choses. Voici un exemple pris dans un programme réel :
let html =
let content = read_whole_file file in
GHtml.html_from_string content
;;
let menu_bold () =
match bold_button#active with
| true -> html#set_font_style ~enable:[`BOLD] ()
| false -> html#set_font_style ~disable:[`BOLD] ()
;;
let main () =
(* code omitted *)
factory#add_item "Cut" ~key:_X ~callback: html#cut
;;
Dans ce vrai morceau de code, html
est un "widget" ("contrôle")
d'édition de HTML (un objet provenant de la bibliothèque lablgtk), créé
une fois pour toute au début du programme par la première instruction
let html =
. Il y est ensuite fait référence dans plusieurs des
fonctions suivantes.
A noter que le nom html
dans le bout de programme ci-dessus ne devrait
pas être comparé à une vraie variable globale comme en C ou dans
d'autres langages impératifs. Aucun emplacement n'est réservé pour
"stocker" le "pointeur sur html
". Il n'est pas non plus possible
d'assigner quelque chose à html
, par exemple pour lui faire désigner
un autre widget. Dans la section suivante nous parlerons des références,
qui sont de vraies variables.
Let-binding (?)
Les utilisations de let ...
aussi bien au niveau global que dans une
fonction, sont souvent appellées des let-binding.
Références : les vraies variables
Comment faire si vous voulez une vraie variable que vous pouvez assigner puis changer à votre gré dans votre programme ? Il vous faut une référence. Les références sont similaires aux pointeurs en C/C++. En Java, toutes les variables qui stockent des objets sont en fait des références (pointeurs) sur ces objets. En Perl, les références sont des références - comme en OCaml.
Voici comment créer une référence sur un entier (int
) en OCaml :
# ref 0;;
- : int ref = {contents = 0}
En fait cette instruction n'est pas très utile. Nous avons créé une référence et puis, comme elle n'a pas de nom, le glaneur de cellules a fait son travail et l'a immédiatement libérée! (Il est même probable que le compileur ait éliminé cette instruction à la compilation). Donnons un nom à cette référence :
# let my_ref = ref 0;;
val my_ref : int ref = {contents = 0}
Cette référence contient maintenant l'entier 0. Mettons une autre valeur à la place (assignement) :
# my_ref := 100;;
- : unit = ()
Voyons ce que la référence contient à présent :
# !my_ref;;
- : int = 100
Donc l'opérateur :=
est utilisé pour assigner les références, et !
pour les déréférencer et accéder au contenu. Voici une comparaison
approximative avec C/C++ :
OCaml C/C++
let my_ref = ref 0;; int a = 0; int *my_ptr = &a;
my_ref := 100;; *my_ptr = 100;
!my_ref *my_ptr
Les références ont leurs usages, mais vous vous apercevrez que vous ne
les utiliserez pas si souvent. La plupart du temps, vous utiliserez
let nom = expression in
pour nommer des expressions locales dans vos
définitions de fonctions.
Fonctions imbriquées
Le langage C n'a pas réellement de notion de fonctions imbriquées. GCC les supporte, mais je ne connais pas de programme qui en tire parti. En tout cas, voici ce que la page info de gcc dit sur les fonctions imbriquées :
Une « fonction imbriquée » est une fonction définie à l'intérieur d'une
autre fonction (les fonctions imbriquées ne sont pas supportées par GNU
C++.) Le nom de la fonction imbriquée est local au bloc dans lequel il
est défini. Par exemple, définissons une fonction imbriquée nommée
square
, et appelons-là deux fois :
foo (double a, double b)
{
double square (double z) { return z * z; }
return square (a) + square (b);
}
La fonction imbriquée a accès à toutes les variables de la fonction
englobante qui sont visibles à l'emplacement de sa définition. Cela
s'appelle la « portée lexicale ». Par exemple, voici une fonction
imbriquée qui utilise une variable héritée nommée offset
:
bar (int *array, int offset, int size)
{
int access (int *array, int index)
{ return array[index + offset]; }
int i;
/* ... */
for (i = 0; i < size; i++)
/* ... */ access (array, i) /* ... */
}
Vous comprenez l'idée. Les fonctions imbriquées sont, cependant, très utiles et largement employées en OCaml. Voici un exemple d'utilisation réel de fonction imbriquées :
# let read_whole_channel chan =
let buf = Buffer.create 4096 in
let rec loop () =
let newline = input_line chan in
Buffer.add_string buf newline;
Buffer.add_char buf '\n';
loop ()
in
try
loop ()
with
End_of_file -> Buffer.contents buf;;
val read_whole_channel : in_channel -> string = <fun>
Ne vous inquiétez pas de ce que ce code fait — il utilise beaucoup de
concepts que nous n'avons pas encore vu dans ce tutorial.
Concentrez-vous plutôt sur la fonction imbriquée principale nommée
loop
qui ne prend qu'un argument unit. On peut appeler loop ()
depuis l'intérieur de la fonction read_whole_channel
, mais elle n'est
pas définie en dehors de cette fonction. La fonction imbriquée peut
accéder aux variables définies dans la fonction principale (ici loop
accède au nom local buf
.)
La syntaxe des fonctions imbriquées est la même que pour nommer les
expressions locales : let nom arguments = définition de fonction in
.
Normalement, vous indenterez la définition de fonction, après être passé
à la ligne comme dans l'exemple précédent, et n'oubliez pas d'utiliser
let rec
à la place de let
si votre fonction est récursive (comme
dans cet exemple).
Modules et open
OCaml est accompagné d'une quantité de modules amusants et intéressants,
de bibliothèques de code utile. Par exemple on y trouve une bibliothèque
pour dessiner des graphismes, interagir avec la collection de contrôles
de l'interface-utilisateur graphique, pour manipuler des grands nombres,
des structures de données, ou pour faire des appels systèmes POSIX. Ces
bibliothèques se trouvent dans /usr/lib/ocaml/
(sous Unix en
tout cas). Pour ces exemples, nous allons nous concentrer sur un module
relativement simple appelé Graphics
.
Le module Graphics
est constitué de sept fichiers (sur mon système) :
/usr/lib/ocaml/graphics.a
/usr/lib/ocaml/graphics.cma
/usr/lib/ocaml/graphics.cmi
/usr/lib/ocaml/graphics.cmx
/usr/lib/ocaml/graphics.cmxa
/usr/lib/ocaml/graphics.cmxs
/usr/lib/ocaml/graphics.mli
Pour l'instant occupons-nous de graphics.mli
. C'est un fichier texte,
que vous pouvez donc lire dès à présent. Notez que son nom est
graphics.mli
et non Graphics.mli
. OCaml met automatiquement une
capitale au nom du fichier pour obtenir le nom du module. C'est
déroutant quand on ne le sait pas !
Il y a deux moyens pour utiliser les fonctions de Graphics
. Ou bien on
commence son programme par la déclaration open Graphics;;
, ou bien on
préfixe tous les appels de fonctions comme ceci : Graphics.open_graph
.
open
ressemble un peu à l'instruction import
en Java, ou plus encore
comme l'instruction use
en Perl.
Pour utiliser les module Graphics
dans la boucle interactive, il
faut au préalable charger la librairie avec
#load "graphics.cma";;
Une paire d'exemples devrait éclaircir ce point. (Les deux exemples
dessinent des choses différentes — essayez-les). Remarquez que le
premier exemple appelle open_graph
et le second Graphics.open_graph
.
(* To compile this example: ocamlc graphics.cma grtest1.ml -o grtest1 *)
open Graphics;;
open_graph " 640x480";;
for i = 12 downto 1 do
let radius = i * 20 in
set_color (if (i mod 2) = 0 then red else yellow);
fill_circle 320 240 radius
done;;
read_line ();;
(* To compile this example: ocamlc graphics.cma grtest2.ml -o grtest2 *)
Random.self_init ();;
Graphics.open_graph " 640x480";;
let rec iterate r x_init i =
if i = 1 then x_init
else
let x = iterate r x_init (i-1) in
r *. x *. (1.0 -. x);;
for x = 0 to 639 do
let r = 4.0 *. (float_of_int x) /. 640.0 in
for i = 0 to 39 do
let x_init = Random.float 1.0 in
let x_final = iterate r x_init 500 in
let y = int_of_float (x_final *. 480.) in
Graphics.plot x y
done
done;;
read_line ();;
Ces deux exemples utilisent des fonctionnalités dont nous n'avons pas encore parlé : boucles "for" dans le style impératif, if-then-else, et récursion. Nous en parlerons plus tard. Examinez-les cependant, et essayez de comprendre (1) comment ils fonctionnent, et (2) comment l'inférence de type aide à éliminer des bugs.
Le module Pervasives
Il y a un module que vous n'avez jamais besoin d'ouvrir avec open
,
c'est le module Pervasives
(allez jeter un oeil sur
/usr/lib/ocaml/VERSION/pervasives.mli
). Tous les symboles définis dans
le module Pervasives
sont implicitement importés dans tous les
programmes OCaml.
Renommage de modules
Que faire si vous voulez utiliser des symboles en provenance du module
Graphics
, mais ne voulez pas tous les importer, et n'avez pas envie de
taper Graphics.
à chaque fois ? Renommez le module avec cette astuce :
module Gr = Graphics;;
Gr.open_graph " 640x480";;
Gr.fill_circle 320 240 240;;
read_line ();;
Cela devient vraiment utile quand vous voulez utiliser les symboles d'un module imbriqué (les modules peuvent être imbriqués les uns dans les autres, eux-aussi), mais ne voulez pas avoir à taper à chaque fois le chemin d'accès au module imbriqué.
L'opérateur de séquence ;
Le point-virgule ;
est un opérateur, comme +
. Bon, pas tout à
fait comme +
mais conceptuellement le même. L'opérateur +
a pour
type int -> int -> int
— il prend deux entiers et retourne un entier
(la somme des deux entrées). Le point-virgule ;
peut être vu comme
ayant le type unit -> 'b -> 'b
— il prend deux valeurs et simplement
retourne la seconde, la première expression étant garantie d'être
évaluée avant la seconde. C'est donc comme l'opérateur ,
(virgule)
en C. Vous pouvez écrire a; b; c; d
de la même manière que vous
pouvez écrire a + b + c + d
.
C'est un de ces « sauts conceptuels » qui n'est pas toujours explicité
très bien : en OCaml, presque tout est une expression. if/then/else
est une expression. a; b
est une expression. match foo with ...
est une expression. Le code qui suit est parfaitement légal (et
toutes les lignes font la même chose);
# let f x b y = if b then x+y else x+0
let f x b y = x + (if b then y else 0)
let f x b y = x + (match b with true -> y | false -> 0)
let f x b y = x + (let g z = function true -> z | false -> 0 in g y b)
let f = fun x -> fun b -> fun y -> if b then x+y else x+0
let f x b y = x + (let _ = y + 3 in (); if b then y else 0);;
val f : int -> bool -> int -> int = <fun>
Notez particulièrement la dernière ligne où j'utilise l'opérateur ;
pour « joindre » deux instructions. Toutes les fonctions en OCaml
peuvent être déclarées sous la forme :
let name [parameters] = expression ;;
En OCaml, la définition de ce qu'est une expression est simplement un
peu plus large qu'en C. En fait, C a le concept d'« instructions » —
mais toutes les instructions en C sont simplement des expressions en
OCaml de type unit
(combinées avec l'opérateur ;
).
La différence entre ;
et +
est qu'on peut utiliser +
comme une
fonction. Par exemple, on peut définir une fonction sum_list
, qui
somme une liste d'entier, par
# let sum_list = List.fold_left ( + ) 0;;
val sum_list : int list -> int = <fun>
La disparition de ;;
Maintenant, nous allons nous pencher sur un point très important.
Tous les exemples ci-dessus finissaient par un double point-virgule
;;
. Cependant, si vous regardez à du code OCaml en dehors de ces
tutoriaux, vous trouverez de codes complets qui n'utilisent pas ;;
,
pas même une seule fois.
La vérité est que ;;
est principalement utilisé dans la boucle
interactive (toplevel) et les tutoriaux pour marquer la fin d'une
phrase OCaml et l'envoyer au toplevel pour évaluation.
En dehors du toplevel, l'usage de ;;
est, au mieux, infréquent et
n'est jamais requis pour du code bien écrit.
En bref, le double point-virgule ;;
peut être utilisé pour trois
raisons :
- compatibilité avec le toplevel ;
- couper le code pour faciliter de débogage;
- introduire une expression « au plus haut niveau ».
Insérer ;;
peut parfois se révéler utile pour les débutants lors du
débogage vu qu'il conclut la définition en cours. Dans l'exemple
suivant, la définition de f
ne s'arrête pas à la ligne 1 à cause de
la virgule ,
. Par conséquent, le compilateur va générer une erreur
à la fin de la seconde ligne :
let f x = x,
let g = x * x
Insérer un double point-virgule entre f
et g
sépare les
définitions de f
de g
:
let f x = x,
;;
let g = x * x
Un autre usage de ;;
est d'introduire une expression après des
définitions :
let b = "This started with"
let s = "a very nonsensical message.";;
print_endline b; print_endline s
open String
let s = concat "" ["Fortunately"; ", "; "the"; "end"; "is"; "near"; "."];;
print_endline s;;
let s = "let end here" in print_endline s
En particulier, dans les exemples ci-dessus, toutes les lignes
démarrant après un ;;
produisent des effets de bord et les effacer
changerait uniquement l'effet du code, pas des définitions.
Cependant, cette utilisation de ;;
peut (devrait) toujours être
remplacée par soit
let () = expression ()
si le résultat est de type unit
, soit
let _ = expression ()
sinon. Notez que la première forme est plus sure puisqu'elle requiert
que le type retourné par l'expression est unit
, nous prémunissant,
par exemple, de l'oubli d'un argument comme dans
let () =
print_newline
(* Ici, nous avons oublié () et le compilateur va se plaindre. *)
Avec cette convention, il n'y a plus d'expressions « au plus haut
niveau » : tout module peut être écrit comme une suite de
définitions. Par conséquent, des directives de style considèrent que
;;
ne devrait jamais être utilisé en dehors de la boucle interactive
(voir par exemple « style guidelines »).
Toutes ces notions ensemble : un exemple de code réel
Dans cette section nous allons regarder quelques fragments de vrai code
pris dans la bibliothèque lablgtk 1.2. (Lablgtk est la bibliothèque
OCaml d'interfaçage avec la bibliothèque de widgets Gtk, native sous
Unix). Un avertissement : ces fragments utilisent beaucoup de concepts
que nous n'avons pas encore abordé. Ne vous arrêtez pas sur les détails,
regardez plutôt la forme générale du code, comment les auteurs ont
utilisé ;;
, où ils ont utilisé ;
et où ils ont utilisé open
,
comment ils ont indenté le code, et utilisé des expressions nommées
locales ou globales.
... Je vais quand même vous donner quelques indices pour que vous ne soyez pas totalement perdus !
?foo
et~foo
est la façon de passer des paramètres optionels et nommés en OCaml. Il n'y a pas vraiment d'équivalent dans les langages de la famille de C, mais Perl, Python et Smalltalk permettent tous de nommer les arguments d'un appel de fonction, en oublier certains, et fournir les autres dans n'importe quel ordre.foo#bar
est un appel de méthode (appel de la méthode nomméebar
de l'objet nomméfoo
). C'est l'équivalent defoo->bar
, oufoo.bar
ou$foo->bar
en C++, Java et Perl, respectivement.
Premier fragment : Le programmeur ouvre une paire de modules standards
(en omettant le ;;
parce que les mots clés suivants sont open
et
let
, respectivement). Il crée ensuite une fonction nommée
file_dialog
. Dans cette fonction il définit une expression nommée
appelée sel
en utilisant une instruction let sel = ... in
de deux
lignes. Puis il appelle plusieurs méthodes de sel
.
(* First snippet *)
open StdLabels
open GMain
let file_dialog ~title ~callback ?filename () =
let sel =
GWindow.file_selection ~title ~modal:true ?filename () in
sel#cancel_button#connect#clicked ~callback:sel#destroy;
sel#ok_button#connect#clicked ~callback:do_ok;
sel#show ()
Deuxième fragment : Juste une longue liste de définitions au niveau
global. Remarquez que l'auteur a omis toutes les occurrences de ;;
grâce à la règle n°2.
(* Second snippet *)
let window = GWindow.window ~width:500 ~height:300 ~title:"editor" ()
let vbox = GPack.vbox ~packing:window#add ()
let menubar = GMenu.menu_bar ~packing:vbox#pack ()
let factory = new GMenu.factory menubar
let accel_group = factory#accel_group
let file_menu = factory#add_submenu "File"
let edit_menu = factory#add_submenu "Edit"
let hbox = GPack.hbox ~packing:vbox#add ()
let editor = new editor ~packing:hbox#add ()
let scrollbar = GRange.scrollbar `VERTICAL ~packing:hbox#pack ()
Troisième fragment : L'auteur importe tous les symboles du module
GdkKeysyms
. Puis nous avons un let-binding inhabituel.
let _ = expression
signifie "évalue la valeur de expression (avec tous
les effets de bords que cela comporte), puis jette le résultat". En
l'occurrence, "calcule la valeur de l'expression" signifie exécuter
Main.main ()
qui est la boucle principale de Gtk, qui a pour effet de
bord d'ouvrir la fenêtre de l'application à l'écran. Le "résultat" de
Main.main ()
est sans importance - probablement la valeur unit
, mais
je n'ai pas vérifié - et elle n'est retournée que quand l'application
est en train de se terminer.
Remarquez que dans ce fragment nous avons de longues successions de commandes procédurales. C'est un programme impératif classique.
(* Third snippet *)
open GdkKeysyms
let _ =
window#connect#destroy ~callback:Main.quit;
let factory = new GMenu.factory file_menu ~accel_group in
factory#add_item "Open..." ~key:_O ~callback:editor#open_file;
factory#add_item "Save" ~key:_S ~callback:editor#save_file;
factory#add_item "Save as..." ~callback:editor#save_dialog;
factory#add_separator ();
factory#add_item "Quit" ~key:_Q ~callback:window#destroy;
let factory = new GMenu.factory edit_menu ~accel_group in
factory#add_item "Copy" ~key:_C ~callback:editor#text#copy_clipboard;
factory#add_item "Cut" ~key:_X ~callback:editor#text#cut_clipboard;
factory#add_item "Paste" ~key:_V ~callback:editor#text#paste_clipboard;
factory#add_separator ();
factory#add_check_item "Word wrap" ~active:false
~callback:editor#text#set_word_wrap;
factory#add_check_item "Read only" ~active:false
~callback:(fun b -> editor#text#set_editable (not b));
window#add_accel_group accel_group;
editor#text#event#connect#button_press
~callback:(fun ev ->
let button = GdkEvent.Button.button ev in
if button = 3 then begin
file_menu#popup ~button ~time:(GdkEvent.Button.time ev); true
end else false);
editor#text#set_vadjustment scrollbar#adjustment;
window#show ();
Main.main ()