fr » modules

Modules

Usage standard

Avec OCaml, tout programme est contenu dans un module. Un module peut même être , plus rarement, un sous-module d'un autre, à la manière d'une arborescence de dossiers imbriqués les uns dans les autres.

Lorsqu'on écrit un programme, avec par exemple deux fichiers __amodule.ml__ et __bmodule.ml__, ceux ci définissent deux modules, intitulés respectivement __Amdodule__ et __Bmodule__, possédant le même contenu que nos fichiers.

Prenons par exemple le fichier amodule.ml suivant:

let hello () = print_endline "Hello"

ainsi que le fichier bmodule.ml :

Amodule.hello ()

En général, les fichiers sont compilés un par un, comme ceci:

ocamlopt -c amodule.ml
ocamlopt -c bmodule.ml
ocamlopt -o hello amodule.cmx bmodule.cmx

Nous avons à présent un petit éxécutable pouvant afficher "Hello". Comme on l'a constaté, on accède à n'importe quoi dans un module donné en appellant le nom du module (qui commence toujours par une majuscule) suivit d'un point et du nom de l'objet utilisé. Cet object peut être une variable, un type ou tout autre chose définie au sein du module.

Les librairies (bibliothèques), notament la bibliothèque standard, fournit un certain nombre de modules. Par exemple, __List.iter__ désigne la fonction __iter__ définie au sein du module __List__. Si jamais on souhaite utiliser de façon intensive un module, on peut désirer se passer de l'appel de son nom; il suffit pour cela d'utiliser l'instrution __open__. En poursuivant notre exemple, __bmodule.ml__ pourrait contenir :

open Amodule;;
hello ();;

Notons que beaucoup de programmeurs oublient les ";;", il est plus courant d'écrire

open Amodule
let _ = hello ()

L'usage de l'instruction __open__ n'est qu'une question de goût. Certains modules proposent des noms déjà utilisés dans d'autres modules. Ainsi le module __List__ contient des fonctions aux noms assez courants dans les autres modules, aussi on n'écrit assez rarement __open List__. D'autres comme __Printf__ utilise des noms très spécifiques, suffisament rares pour ne pas créer de conflits avec d'autres modules; pour éviter d'écrire __Printf.printf__ on préfera utiliser __open Printf__ en début de fichier:

open Printf
let my_data = [ "a"; "beautiful"; "day" ]
let _ = List.iter (fun s -> printf "%s\n" s) my_data

Interfaces modulaires et signatures.

Un module peut fournir un grand nombre d'éléments (fonctions, types, sous modules, variables...) au programme qui l'utilise. Par défaut, tous les objets définis au sein du module seront accessibles depuis "l'exterieur". Ceci peut être utile dans de petits programmes, mais il est préférable qu'un module ne propose que ce qu'il est conçu pour proposer, sans le fouillis des fonctions auxilliaires ou variables temporaires. Pour cela, nous pouvons définir une interface modulaire, qui se comporte comme un filtre entourant le module. De la même façon qu'un module vient d'un fichier __.ml__, l'interface corespondante (et sa signature) viennent d'un fichier __.mli__ . Il contient la liste des valeurs avec leur type (ou signature pour les fonctions). Réécrivons notre __amodule.ml__ :

let message = "Hello"
let hello () = print_endline message

Tel quel, l'interface de __Amodule__ est la suivante:

val message : string
val hello : unit -> unit

Supposons que nous souhaitions que personne de l'exterieur ne puisse accéder à la variable __message__. Nous choisissons alors de la cacher en crééant une interface restreinte, le __amodule.mli__ est alors:

val hello : unit -> unit
(** displays a greeting message *)

(Notons que commenter les fichiers __.mli__ en utilisant le format d'__ocamldoc__ est une très bonne habitude à prendre)

Les fichiers __.mli__ doivent être compilés juste avant les __.ml__ correspondants. On peut les compiler à l'aide d'__ocamlc__, même si les __.ml __sont écrit en code natif avec __ocamlopt__

ocamlc -c amodule.mli
ocamlopt -c amodule.ml
...

Types abstraits (virtuels)

Passons à présent aux définitions de types. Nous avons vu que les valeurs telles que les fonctions, peuvent être exportées en donnant leur noms et signatures dans un fichiers __.mli__, par exemple:

val hello : unit -> unit

Cependant on définit souvent au sein d'un module de nouveaux types. Prenons par exemple un enregistrement d'une date:

type date = { day : int;
              month : int;
              year : int }

Il n'y a plus 2 mais 4 options possibles dans l'écriture du fichier __.mli__:

1. Le type n'est pas précisé dans le __.mli__ 2. La définition du type est copiée-collée dans le __.mli__ 3. Le type est rendu virtuel, seul son nom est précisé. (il est connu du compilateur mais on ne peut y toucher) 4. Les champs d'un enregistrement sont rendus seulement lisibles et non plus modifiables (lecture seule) __type date = private { ... }__

Dans le troisieme cas, on obtient le code suivant:

type date

Les utilisateurs de ce module peuvent désormais manipuler des données de type __date__ mais ne peuvent accéder aux champs de l'enregistrement, seules les fonctions du modules sont autorisées à le faire. Supposons que notre module contienne trois fonctions, une pour créer une date, une pour calculer la différence entre deux dates et une pour convertir une date en années:

type date
val create : ?days:int -> ?months:int -> ?years:int -> unit -> date
val sub : date -> date -> date
val years : date -> float

On remarque alors que seule __create__ et __sub_ peuvent être utilisées pour céer des enregistrements de dates. Ainsi il n'est pas possible pour l'utilisateur de créer des enregistrements difformes. Ainsi, bien que notre implémentation utilise un enregistrement nous pouvons le modifier sans qu'aucun fichier utilisant ce module n'en soit perturbé. Ceci est particulièrement utile dans le cas des librairies, qui peuvent ainsi être modifiées tout en gardant une utilisation identique.

Sous modules

Création d'un sous module

Nous avons vu qu'un fichier unique __exemple.ml__ se compilait un un module unique __Exemple__. Sa signature est automatiquement générée et est la plus exhaustive possible, sauf si elle est restreinte par l'écriture d'un fichier __.mli__ . Ceci dit, un module donné peut etre définit explicitement au sein même d'un fichier, il est ainsi un sous module du module principal. Prenons l'exemple suivant:

module Hello = 
struct
  let message = "Hello"
  let hello () = print_endline message
end
let goodbye () = print_endline "Goodbye"
let hello_goodbye () =
  Hello.hello ();
  goodbye ()

Depuis un autre fichier, nous avons désormais deux niveaux de modules. Nous pouvons écrire alors:

let _ =
  Example.Hello.hello ();
  Example.goodbye ()
 
 

Interface submodulaire

Nous pouvons de même restreindre l'interface d'un sous module. On appelle cela un type modulaire. Dans notre __exemple.ml__ cela donne:

module Hello : 
sig
 val hello : unit -> unit
end = 
struct
  let message = "Hello"
  let hello () = print_endline message
end
(* At this point, Hello.message is not accessible anymore. *)
let goodbye () = print_endline "Goodbye"
let hello_goodbye () =
  Hello.hello ();
  goodbye ()

La définition du module __Hello__ est équivalente au couple de fichiers __hello.mli/hello.ml__ . Cependant, écrire tout cela dans un même bloc de code n'est pas très élégant, nous préférons donc définir la signature séparement.

 
module type Hello_type =
sig
 val hello : unit -> unit
end
module Hello : Hello_type =
struct
  ...
end

__Hello_type__ est un type modulaire et peut donc être réutilisée pour définir d'autres interfaces de modules.

Bien que l'utilisation des sous modules peut se révéler pratique dans certains cas spécifiques, leur utilité réelle se révèle avec les foncteurs, développés dans la section suivante.

Foncteurs

Les foncteurs sont probablement une des fonctionnalités les plus complexes d'OCaml, même s'il n'est pas nécessaire de les utiliser de façon intensive pour être un bon programmeur. En réalité, il est possible que nous n'ayons jamais à définir nous même de foncteurs, mais nous serons surement appelés à les rencontrer dans la librairie standard. Ils sont la seule façcon d'utiliser les modules __Set__ et __Map__, mais leur utilisation reste à notre portée.

Un foncteur est un module qui est paramétré par un autre module, tout comme une fonction n'est qu'une valeur paramétrée par d'autre valeurs (les arguments). En gros, cela permet de paramétrer un type par une valeur, ce qui est impossible à faire directment en OCaml. Par exemple, nous pourrions définir un foncteur prenant un entier __n__ et retournant un ensemble d'opérations sur des tableaux de longeurs __n__ uniquement. Si par erreur, un programmeur donne un tableau normal à une de ces fonctions, le compilateur soulevera une erreur. Si nous n'utilisions pas un foncteur mais le type standard des tableaux, le compilateur ne sera pas capable de détecter l'erreur, et nous obtiendrions une erreur à l'execution bien après la compilation ce qui est bien pire !

Comment utiliser un foncteur ?

La librairie standard définit le module __Set__, qui fournit un foncteur __Make__. Ce foncteur requiert un argument, qui est un module contenant (au moins) deux choses: le type des éléments donné par __t__ et la fonction de comparaison donnée par __compare__ . L'important est de s'assurer que la même fonction de comparaison sera toujours utilisée, même si le programmeur commet une erreur.

Par exemple, si nous voulons un ensemble d'entiers, il faut utiliser ceci:

module Int_set = Set.Make (struct
                             type t = int
                             let compare = compare
                           end) 

Pour les ensembles de chaînes, cela est même plus simple car la librairie standard fournit un module __String__ avec un type __t__ et une fonction __compare__. On peut donc créer, à peu de frais, un module pour la manipulation des ensembles de chaînes de caractères:

module String_set = Set.Make (String)

(les parenthèses sont obligatoires !)

Comment définir un foncteur ?

Un foncteur avec un argument peut être définit comme ceci:

module F (X : X_type) =
struct
 ...
end

où __X__ est le module passé en argument, et __X_type__ est sa signature, qui est obligatoire.

La signature du module obtenu peut elle même être restreinte à l'aide la syntaxe habituelle:

module F (X : X_type) : Y_type =
struct
  ...
end

ou bien en le spécifiant dans le fichier __.mli__:

module F (X : X_type) : Y_type

La syntaxe des foncteurs reste cependant difficile à assimiler. Il est donc préférable de jeter un coup d'oeil aux fichier sources __set.ml__ ou __map.ml__ dans la librairie standard. Une dernière remarque: les foncteurs ont été conçus pour aider les programmeurs et non pas pour améliorer les performances. L'execution est même plus lente, à moins d'utiliser un défoncteur comme __ocamldefun__, qui requiert un accès au code source du foncteur.

Manipulation pratique des modules

Afficher l'interface d'un module

En __toplevel__, il est possible de visualiser le contenu d'un module en tapant:

# module M = List;;

On obtient alors:

module M :
  sig
    val length : 'a list -> int
    val hd : 'a list -> 'a
    val tl : 'a list -> 'a list
    val nth : 'a list -> int -> 'a
    val rev : 'a list -> 'a list
    ...
  end
 

De toute façon, il existe une documentation pour la plupart des modules (on peut aussi utiliser __ocamlbrowser__, fourni avec __labltk__)

Insertion dans un module

Supposons que nous sentions qu'une fonction manque au module standard __List__ et que nous désirions qu'elle en fasse partie intégrante. Il est possible d'utiliser l'instruction __include__ au sein d'un fichier __extensions.ml__ afin d'insérer notre fonction:

module List =
struct
 include List
 let rec optmap f = function
     [] -> []
   | hd :: tl ->
       match f hd with
           None -> optmap f tl
         | Some x -> x :: optmap f tl
end

Nous avons créé un nouveau module __Extensions.List__ contenant , en plus des fonctions habituelles du module __List__, une nouvelle fonction __optmap__. Depuis un autre fichier, il nous suffit d'ouvrir notre module __Extensions__ pour que celui ci 'écrase' le module __List__ standard:

open Extensions
...
List.optmap ...