Cours 1

Htmligure
Version & licenses
Creative Commons License

Modules et utilisation de librairies

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Nous avons vu avec le premier exemple que les programmes en OCaml sont organisés en modules, et que chaque module est une liste de types, de valeurs, de modules et de types de modules. En particulier, nous avons mentionné que chaque fichier est un module, portant le même nom que le fichier. Ceci explique que les noms des fichiers de source OCaml doivent être formés de caractères alphanumériques et du caractère _.

Définir des modules

Il est aussi possible de définir des modules internes à un fichier. Plus généralement, tout module peut contenir un autre module. On utilise pour cela la syntaxe de définition d'un module :

  1. module NameOfModule = struct

  2. (* list of types, values, modules and interfaces *)

  3. end

Le nom d'un module doit être formé de caractères alphanumériques et du caractère _, et doit impérativement commencé par une majuscule.

Interfaces

Les modules sont eux aussi typés. Le type d'un module, qui est appelé interface du module, est une liste de déclarations comprenant :

  • des noms des types définis par le module, avec ou sans leur définition,
  • des noms des valeurs définies par le module, avec leurs types,
  • des noms des modules définis par le module, avec leur interface,
  • des interfaces définis par le module, avec ou sans leur définition.

L'interface d'un module peut ignorer une partie des définitions présentes dans le module : ces définitions omises ne seront pas accessibles depuis l'extérieur du module : c'est exactement comme si elles n'existaient pas.

En ce qui concerne les noms des types, chaque type apparaissant dans l'interface peut apparaître sous trois formes : avec, avec ou sans la définition associée. Les trois syntaxes sont :

  1. type name_of_type
  2. type name_of_type = private type_definition
  3. type name_of_type = type_definition

Dans le premier cas, seul le nom du type sera connue à l'extérieur du module. Les valeurs de ce type ne pourront donc être manipulées que par les fonctions définies par le module : il n'y a pas d'accès possible à la représentation des valeurs de ce type.

Dans le troisième cas, l'expression du type est exportée, donc les modules utilisant ce type peuvent le manipuler en connaissant sa représentation, donc sans limite.

Le deuxième cas est intermédiaire, il permet d'exporter la représentation, mais d'interdire la création de valeur de ce type à l'extérieur du module. Par rapport au premier cas, l'intérêt est d'autoriser l'observation des valeurs de ce type (de la même façon que nous avons vu comment observer un couple). Nous en reparlerons donc lorsque nous étudierons les types en détails.

En ce qui concerne les valeurs, la syntaxe utilisée est :

  1. val identifier : type_expression

L'expression de type ne peut utiliser que des types connus à l'extérieur du module. Si l'expression de type utilise des types définis par le module, ceux-ci doivent être déclarés dans l'interface.

En ce qui concerne les modules, chaque module $I$ définie à l'intérieur d'un module $O$ peut être déclaré dans l'interface de $O$, en indiquant le nom de $I$ et son interface. Il est possible de ne pas donner d'interface, mais c'est rarement utile puisque cela rend le module inutilisable (donc peut-être ne fallait-il pas mentionner ce module dans l'interface de $O$). Les syntaxes possibles sont :

  1. module NameOfModule = sig
  2. (* a list of declarations of types, values, modules and interfaces *)
  3. end

  4. module NameOfModule = Name_of_an_interface

Il est possible de nommer une interface, en utilisant le mot-clé module type. Prenons cet exemple, avec l'équivalent de l'interface Comparable de Java, et un module implémentant cette interface :

  1. (* in .ml file : *)
  2. module type Comparable = sig
  3. type t
  4. val compare : t -> t -> int
  5. end

  6. (* Then we can write : *)
  7. module IntComparable =
  8. type t = int
  9. let compare n p = n - p
  10. end
  1. (* in .mli file : *)
  2. module type Comparable = sig
  3. type t
  4. val compare : t -> t -> int
  5. end

  6. module IntComparable : Comparable

De fait, Comparable existe dans la librairie standard d'OCaml, mais s'appelle Set.OrderedType. Notons ici que IntComparable est déclaré avec une interface nommée, en l'occurence Comparable : on peut utiliser les interfaces nommées à la place d'interfaces explicites (celles données avec sig ... end).

Utiliser un module

Toute valeur, ou tout type défini par un module et déclaré dans son interface peut être utilisé sans restriction (sauf en présence du mot-clé private) à l'extérieur de ce module. Pour faire référence à un identifiant (de valeur, de type, de module ou d'interface) d'un autre module, il suffit d'utiliser la syntaxe :

  1. Name_Of_Module.identifier

  2. (* In the case of nested modules, for instance : *)
  3. Name_Of_Outer_Module.Intermediate_Module.Inner_Module.identifier

Par exemple, pour définir un cercle à l'extérieur du module Geometry du premier exemple, on peut écrire :

  1. let my_circle =
  2. Geometry.Circle.create
  3. ~center:(Geometry.vector 2. (-1.))
  4. ~radius:3.

On peut éviter de devoir redonner systématiquement le nom d'un module en ouvrant ce module :

  1. open Geometry
  2. let my_circle =
  3. Circle.create
  4. ~center:(vector 2. (-1.))
  5. ~radius:3.

Il ne faut cependant pas en abuser, car les noms des modules participent à la documentation, donc à la lisibilité du code. On préfère plutôt ouvrir les modules localement à une expression, en utilisant l'une des deux syntaxes :

  1. let my_circle =
  2. Geometry.(
  3. Circle.create
  4. ~center:(vector 2. (-1.))
  5. ~radius:3.
  6. )

ou

  1. let my_circle =
  2. let open Geometry in
  3. Circle.create
  4. ~center:(vector 2. (-1.))
  5. ~radius:3.

Dans l'interpréteur

Pour utiliser un module externe dans l'interpréteur, s'il ne s'agit pas d'un module du noyau de la librairie standard, il faut le charger explicitement. L'interpréteur accepte des directives pour cela. Les directives sont des mot-clés propres à l'interpréteur (ne faisant pas partie du langage OCaml) commençant par le caractère #, et permettant de paramétrer l'interpréteur.

Pour charger un module, la façon basique est d'utiliser #load "filename.cmo";; avec filename.cmo un fichier compilé. Il est nécessaire d'avoir le répertoire contenant le fichier dans la liste des répertoires recherchés par le toplevel. Pour ajouter un répertoire, il suffit de faire #directory "/directory_path";;. Si l'opération échoue, le toplevel affiche un message d'erreur.

  1. #directory "/directory_path";;
  2. #load "filename.cmo";;

Si le module à charger fait partie d'une librairie installée, il existe une méthode plus simple :

  1. #use "topfind";; (* only once, findlib must be installed *)
  2. #require "package_name";; (* for each package *)

Pour la compilation

Pour compiler un module faisant référence à d'autres modules, il faut donner la liste de ces modules en arguments. C'est bien sûr fastidieux, ce qui explique l'usage d'outils de compilation, comme make. Nous utiliserons ocamlbuild, qui est relativement simple à utiliser pour les projets de taille moyenne, donc suffisant pour nos besoins, et ocamlfind qui est un utilitaire pour trouver les emplacements d'installation des librairies dans le système.

Ocamlbuild gère les modules grâce à un fichier de configuration, contenant les informations concernant les fichiers sources du projet, et les librairies utilisées par chaque fichier source. Nous apprendrons à l'utiliser au fur et à mesure de nos besoins. Ocamlbuild s'utilise avec la ligne de commande :

  1. ocamlbuild -use-ocamlfind <target>

Par exemple, pour compiler un fichier ml et son interface, en un module compilé, on veut obtenir un fichier d'extension cmo :

  1. ocamlbuild -use-ocamlfind filename.cmo

Ceci crée un fichier filename.cmo dans un sous-répertoire _build. Si le fichier en question comporte des dépendances à d'autres modules, il faudra créer un fichier de configuration _tags pour ocamlbuild. L'utilisation de ces outils sera vu en TP.

Retour au sommaire.