Objective Caml/Modules

(Redirigé depuis OCaml/Modules)

Cette page est considérée comme une ébauche à compléter . Si vous possédez quelques connaissances sur le sujet, vous pouvez les partager en éditant dès à présent cette page (en cliquant sur le lien « modifier »).

Ressources suggérées : Aucune (vous pouvez indiquer les ressources que vous suggérez qui pourraient aider d'autres personnes à compléter cette page dans le paramètre « ressources » du modèle? engendrant ce cadre)

Jusqu'à présent, nous avons vu comment il était possible d'utiliser OCaml en mode interactif. Ce mode est très utile pour apprendre OCaml, pour tester des extraits ou des fonctions existantes ou pour écrire des programmes qui se comporteront eux-mêmes comme OCaml en mode interactif. Cependant, ce mode n'est généralement pas approprié pour le développement de logiciels que vous voudrez distribuer, qu'il s'agisse d'applications bureau, d'applications serveur web ou d'utilitaires. Bien entendu, OCaml permet aussi d'écrire et de compiler des logiciels autonomes adaptés à toutes ces situations. Le mécanisme employé pour ce faire porte le nom de modules.

Les modules ont plusieurs utilités en OCaml. En plus de permettre de produire des applications exécutables, ils permettent aussi de diviser un programme long en plus petites unités, plus simples à gérer, à documenter et à partager entre plusieurs développeurs -- c'est le concept de modularité. C'est encore ce mécanisme qui est utilisé pour garantir l'indépendance entre parties d'un programme ou pour garantir que certaines parties du programme ne peuvent pas être manipulées par l'extérieur -- c'est la notion d'abstraction.

Ainsi, si nous considérons de nouveau l'exemple des nombres complexes, nous emploierons typiquement les modules pour les raisons suivantes :

  • rassembler la définition du type « nombre complexe » et toutes les fonctions qui agissent sur ce type ;
  • interdire à un utilisateur de construire un nombre complexe dont le module est strictement négatif ;
  • normaliser les nombres complexes pour vous assurer que l'argument est toujours compris entre 0 et 2*pi, même si l'utilisateur a donné un argument inférieur ou supérieur.

De même, dans le cadre des opérations sur les fichiers, la bibliothèque standard d'OCaml utilise des modules pour les raisons suivantes :

  • rassembler toutes les opérations sur les fichiers en un endroit, pour des questions de documentation et de cohérence dans les noms de fonctions ;
  • vérifier une bonne fois pour toutes, lors de l'ouverture du fichier, si le fichier existe et si l'utilisateur a bien le droit de le lire ou/et de l'écrire ;
  • cacher à l'utilisateur le type de données utilisé pour représenter les fichiers, de manière à ce que les développeurs de la bibliothèque standard puissent ultérieurement modifier ce type de données en étant certains de ne pas rendre incorrects les programmes développés à l'aide d'une version donnée de la bibliothèque standard.

Au cours de ce chapitre, nous allons construire et manipuler les modules, ainsi que les informations sur les modules. À partir du chapitre suivant, nous emploierons en permanence les modules fournis avec OCaml Batteries Included.

Objectifs du chapitre

modifier

Ce chapitre vous enseignera

  • comment écrire un programme complet
  • comment compiler un programme
  • comment diviser un programme en modules
  • comment utiliser des modules existants, qu'ils aient été écrits par vous ou par les développeurs de la bibliothèque standard d'OCaml
  • comment compiler une bibliothèque pour la distribuer séparément
  • comment cacher les fonctions, types et valeurs privées d'un programme ou d'un module pour éviter qu'elles soient utilisées par d'autres modules
  • comment générer la documentation d'un programme ou d'une bibliothèque.

Conventions typographiques

modifier

Dans la suite de cet ouvrage, pour différencier les extraits tapés dans le mode calculatrice des codes sources sauvegardés dans des fichiers, nous emploierons deux styles différents.

Pour le mode calculatrice, nous noterons :

# print_endline "Bonjour, le monde" ;;

Le même programme, une fois sauvegardé, sera noté avec le style suivant :

print_endline "Bonjour, le monde" ;;

Tout comme le symbole # du mode calculatrice ne doit pas être effectivement tapé, les numéros de lignes ne font pas partie du code source.

Logiciels utilisés au cours de ce chapitre

modifier

Compilation

modifier

Comme nous l'avons mentionné précédemment, OCaml est un langage compilé, c'est-à-dire un langage qui dispose des outils nécessaires pour transformer un code source en un fichier exécutable autonome. Plusieurs outils de compilation existent, qui à partir d'un fichier source peuvent produire soit un exécutable portable mais non-optimisé, comparable à du code compilé Java (ocamlc et ocamlc.opt), soit un exécutable optimisé mais réservé à une plate-forme, comparable à du code compilé C (ocamlopt et ocamlopt.opt), soit du code source utilisable avec une autre syntaxe (camlp4of, camlp4of.opt, camlp4rf, camlp4rf.opt), soit encore un exécutable modifié pour permettre l'analyse de performances (ocamlcp), de la documentation (ocamldoc) etc.

L'outil principal que nous allons manipuler dans le restant de cet ouvrage est OCamlBuild, dont le rôle est d'automatiser la construction d'un exécutable en gérant intelligemment tous les programmes précédents. Cet outil, comme les précédents, est fourni avec OCaml.

La manière la plus simple de compiler un fichier mon_fichier.ml à l'aide d'OCamlBuild est, dans une ligne de commande, de taper

ocamlfind batteries/ocamlbuild fichier_de_destination

Cette commande invoque la version de OCamlBuild pour OCaml Batteries Included en lui indiquant que nous voulons que, à l'issue de la compilation, le fichier fichier_de_destination soit créé. Selon le nom du fichier de destination, la compilation produira soit un fichier exécutable, soit une des informations résumant le contenu du programme, soit une documentation sous la forme d'une page web, etc.


Dans tous les cas, OCamlBuild examine tous les fichiers OCaml du répertoire, détermine les fichiers qui sont nécessaires pour compiler vos fichiers, lesquels de ces fichiers ont été modifiés et doivent donc être recompilés, puis se charge des étapes de compilation et de liaison. En cas de problème de compilation, OCamlBuild affiche des messages d'erreur comparables à celles de la ligne de commande OCaml.

Pour la majorité des projets, cette utilisation d'OCamlBuild suffit. Nous verrons plus tard comment demander à OCamlBuild de ne construire que certaines parties d'un projet incomplet ou comment générer de la documentation depuis un fichier source.

Édition de code source

modifier

Vous pouvez écrire vos programmes OCaml avec n'importe quel éditeur de texte pour langues occidentales. Par contre, Word ou autre traitement de texte ne vous servira à rien.

Emacs 22

modifier

Emacs est un éditeur de texte pour programmeurs, extrêmement puissant, lui-même programmable et disposant de centaines d'extensions pour des langages de programmation ou des situations particulières. Emacs est disponible gratuitement sur à peu près toutes les plate-formes existantes.

Pour utiliser Emacs avec OCaml, vous aurez besoin de

Une fois que vous avez installé et configuré Emacs et Tuareg, Emacs reconnaîtra et lancera automatiquement Tuareg dès que vous ouvrirez un fichier dont l'extension est .ml ou .mli.

Exécuter un extrait depuis Emacs
modifier

Une fois Tuareg lancé, vous pouvez demander au mode interactif de OCaml d'exécuter une commande en plaçant votre curseur sur la commande et en appliquant la combinaison de touches Contrôle-C-E. La première fois que vous invoquerez le mode interactif, Emacs vous demandera quelle version d'OCaml vous voulez employer.

Répondez

ocamlfind batteries/ocaml
Compiler un projet depuis Emacs
modifier

De même, une fois Tuareg lancé, vous pouvez demander à Emacs de compiler les fichiers sur lesquels vous travaillez. Pour ce faire, appliquez la combinaison de touches Contrôle-C-C. La première fois que vous invoquerez le compilateur, Emacs vous demandera quel compilateur vous voulez employer.

Répondez

ocamlfind batteries/ocamlbuild XXXX

XXXX est le nom du fichier compilé que vous souhaitez obtenir.

VI / Vim

modifier

VI / Vim est un autre éditeur de textes pour programmeurs, tout aussi puissant, programmable, extensible et disponible.

Pour utiliser VI avec OCaml, vous aurez besoin de

Si vous pensez que vous serez amené à éditer des fichiers en ssh sur un serveur, vim est un meilleur choix que emacs.

Programmes complets

modifier

OCaml a une définition très simple de ce qu'est un programme complet : tout extrait de OCaml qui fonctionne en mode calculatrice, une fois sauvegardé dans un fichier avec l'extension .ml, est un programme complet valide.

Ainsi,

print_endline "Bonjour, le monde";;

est un programme complet. On peut même d'ailleurs supprimer le ;;, qui n'est pas utile dans ce cas précis.

print_endline "Bonjour, le monde !"


Pour utiliser ce programme, commençons le sauvegarder, sous le nom bonjour.ml.

Lancer un programme

modifier

Nous pouvons maintenant le lancer notre programme ; pour ce faire :

  • ouvrez un terminal (sous Windows, le terminal s'appelle « Invite de commande ») ;
  • allez dans le répertoire dans lequel vous avez sauvegardé votre fichier bonjour.ml en tapant cd le/nom/du/répertoire ;
  • tapez.
$ ocamlfind batteries/ocaml bonjour.ml

(le symbole $ est affiché par votre terminal, vous n'avez pas besoin de l'écrire — si vous êtes sous Windows, le symbole sera plutôt >)

vous verrez s'afficher

Bonjour, le monde !

Félicitations, vous venez de lancer votre premier programme OCaml.

Compiler et exécuter un programme

modifier

Il existe en fait plusieurs manières de lancer un programme OCaml. La manière directe, que nous venons de voir, s'appelle l'interprétation : OCaml lit le programme, vérifie qu'il a un sens, puis l'exécute immédiatement. Il s'agit de la manière la plus immédiate de tester si un programme s'exécute correctement mais le prix à payer est que le programme s'exécute plus lentement qu'avec les autres méthodes. De plus, dans un certain nombre de cas, il n'est pas raisonnable de demander à l'utilisateur, à chaque fois qu'il veut lancer son programme, de taper une ligne de code aussi surprenante que

$ ocamlfind batteries/ocaml bonjour.ml

L'alternative consiste à compiler le programme, c'est-à-dire à demander à OCaml de transformer votre fichier bonjour.ml en un autre fichier que l'ordinateur comprendra directement. Pour ce faire, toujours dans un terminal, tapez l'une des lignes suivantes :

$ ocamlfind batteries/ocamlbuild bonjour.byte

ou

$ ocamlfind batteries/ocamlbuild bonjour.native

La première de ces lignes demande à OCamlBuild de construire bonjour.byte, c'est-à-dire un fichier exécutable portable, petit, rapide à compiler et théoriquement capable de fonctionner sur n'importe quel type d'ordinateur. La deuxième ligne demande à OCamlBuild de construire bonjour.native, c'est-à-dire un fichier exécutable optimisé pour un ordinateur précis et donc inutilisable sur tout autre type d'ordinateur, plus lent à compiler mais plus rapide à l'exécution.

Dans le reste de cet ouvrage, nous utiliserons généralement des fichiers portables (donc ici bonjour.byte). Ce choix est purement arbitraire.

Dans les deux cas, sauf accident, vous verrez apparaître un indicateur de progression puis, à l'issue de la compilation, un message de la forme

Finished, 3 targets (0 cached) in 00:00:00.

Ceci signifie qu'OCamlBuild a du produire 3 fichiers (dont bonjour.byte), qu'aucun de ces fichiers n'avait déjà été produit lors d'une compilation précédente, et que le temps total de compilation a été arrondi à 0 secondes.

Anecdote OCamlBuild est un peu optimiste dans son affichage des durées de compilation.

Pour lancer le programme compilé, sous MacOS X ou Linux, il suffit alors d'écrire, toujours dans le terminal :

$ ./bonjour.byte

ou, sous Windows

> bonjour.byte

De nouveau, le symbole $ ou < est inscrit par votre terminal, ce n'est pas à vous de le recopier.

Vous obtiendrez alors

Bonjour, le monde !

Théoriquement, vous pourriez aussi ouvrir le programme depuis l'Explorateur de fichiers (sous Windows), le Finder (sous MacOS X), Konqueror, Nautilus ou votre gestionnaire de fichiers préféré (sous Linux), en ouvrant le répertoire qui contient bonjour.byte et en double-cliquant sur l'icône correspondante. Pour ce programme, qui se referme immédiatement dès qu'il a affiché une phrase, cela ne servirait à rien. Cependant, une fois que vous aurez doté votre programme d'une interface graphique ou textuelle, ce sera une manière de le lancer.


Note Il existe encore d'autres manières d'exécuter un programme OCaml, notamment la possibilité de transformer votre fichier bonjour.ml en un script exécutable. Cette fonctionnalité n'est quasiment jamais utilisée car le programme ainsi lancé reste interprété et donc beaucoup plus lent qu'un programme compilé. Nous ne détaillerons donc pas la technique.

Effets et résultats

modifier

Si nous avions tapé le contenu de bonjour.ml depuis le mode calculatrice, nous aurions obtenu quelque chose comme :

# print_endline "Bonjour, le monde !"
Bonjour, le monde !
- : unit = ()

Ici, comme nous venons de le voir, le lancement de bonjour.ml, bonjour.byte ou bonjour.native a uniquement causé l'affichage de

Bonjour, le monde !

De la même manière, dans le mode calculatrice, le nombre 5 est une expression valide, qui produira

# 5;;
- : int = 5

Par contre, si nous exécutons le programme correspondant (donc un fichier qui contiendra en tout et pour tout 5), rien ne s'affichera. Cela ne signifiera pas qu'il y a eu le moindre problème -- de fait, le programme se sera exécuté correctement et aura un résultat. Par contre, comme nous n'avons pas précisé dans le programme que nous voulions afficher ce résultat, le programme n'aura aucun effet visible.

Pour faire afficher le résultat, nous aurions du écrire le programme suivant :

print_int 5

À l'exécution, ce programme calculera 5 et aura pour effet d'afficher cette valeur. Comme print_int a pour type int -> unit, ce programme aura de plus pour résultat (), résultat qui ne sera lui-même pas affiché.

Vocabulaire On désigne sous le nom d'effet observable ou, tout simplement, effet toute influence visible du programme sur le reste du monde. Ainsi, afficher un texte, ouvrir une boîte de dialogue, modifier un fichier ou envoyer un mail constituent autant d'effets. Par opposition, calculer le résultat d'une expression arithmétique (sans l'afficher) ou calculer la longueur d'une liste (sans l'afficher) n'a aucun effet. Par extension, on considère que lire un fichier, réagir à un clic ou télécharger une page web sont encore des effets.

Précautions et limitations

modifier

Séparateurs

modifier

Dans le mode calculatrice, le symbole ;; sert à prévenir OCaml que vous avez fini de taper. Dans un fichier .ml, OCaml considère que vous avez fini de taper une fois qu'il atteint la fin du fichier. L'utilisation du ;; est donc beaucoup plus rare dans un fichier .ml que dans le mode calculatrice. On s'en sert sert surtout pour séparer deux expressions.

Ainsi, le programme suivant est tout à fait légitime

5 + 2;;
print_endline "Bonjour, le monde"

Ce programme commencera par calculer le résultat de 5 + 2, résultat qu'il n'affichera pas, puis il affichera le texte "Bonjour, le monde".

Fichiers portables

modifier

Sur le principe, les fichiers exécutables portables .byte que nous venons de voir et les fichiers modules portables .cmo que nous verrons bientôt sont comparables aux fichiers portables .class de Java.

En raison de priorités de développement différentes dans la conception du langage, les fichiers portables de OCaml sont malheureusement moins portables que leurs homologues en Java, puisque, s'ils sont compatibles avec tous les types d'ordinateur, ils ne sont compatibles qu'avec la version de OCaml et des bibliothèques de programmation qui ont servi à les construire.

L'une des conséquences de ces choix de développement est que, le plus souvent, un programme OCaml sera distribué soit sous la forme d'un exécutable natif optimisé, qui sera indépendant des bibliothèques installées, soit sous la forme de code source.

À retenir

modifier
  • N'importe quel code source OCaml peut être utilisé en tant que programme autonome.
  • Un programme autonome n'affiche pas son résultat.
  • Un programme autonome peut être compilé à l'aide de OCamlBuild ou exécuté à l'aide de ocaml.

Modules

modifier

Comme la quasi-totalité des langages de programmation modernes, OCaml Batteries Included est fourni avec un grand nombre de structures de données et de fonctions de manipulation de données, sous la forme d'une bibliothèque standard. Ces structures et fonctions sont rassemblées selon leur rôle en modules. Ainsi, les fonctions qui servent à manipuler des listes forment le module List, les fonctions qui servent à afficher des informations à l'écran forment le module Print, les fonctions de manipulation de texte encodé en Latin-1 (caractères européens) sont rassemblées dans le module String, les fonctions de manipulation de texte encodé en UTF-8 (caractères internationaux) sont rassemblées dans le module UTF8, etc. Au cours de cette section, nous allons voir comment utiliser les fonctions d'un module puis comment créer de nouveaux modules.

Note En OCaml, les noms de modules commencent toujours par une majuscule. Ceci est une obligation du langage.

Utilisation d'un module

modifier

De base, OCaml est fourni avec plus d'une centaine de fonctions de manipulation des listes. Ainsi, pour calculer la longueur d'une liste, on invoquera la fonction List.length, c'est-à-dire la fonction length du module List :

# List.length ;;
- : 'a list -> int = <fun>

De même, pour déterminer le dernier élément d'une liste, on emploiera la fonction List.last, c'est-à-dire la fonction last du module List :

# List.last ;;
- : 'a list -> 'a = <fun>

List.length et List.last sont des fonctions parfaitement ordinaires, qui peuvent être manipulées comme toutes les autres fonctions. Ainsi, on écrira :

# let longueur = List.length ;;
val longueur : 'a list -> int = <fun>

De la même manière, le module List définit un type 'a t, qui est identique au type 'a list et qui peut se manipuler comme tout autre type :

# type associations = (string * int) List.t;;
type associations = (string * int) Batteries.List.t

Nous reviendrons plus tard sur la raison pour laquelle OCaml a répondu Batteries.List.t et non List.t. Pour le moment, nous nous contenterons d'accepter que 'a Batteries.List.t et 'a List.t désignent bien les mêmes types.

Pour consulter la documentation sur un module, vous pouvez employer la directive #man_module. Comme toutes les directives, #man_module ne peut être utilisée que depuis le mode calculatrice, comme suit :

# #man_module "List";;

Vous pouvez aussi consulter directement la documentation d'une des valeurs ou d'un des types du module

# #man_value "List.length";;
# #man_type "List.t";;

Vous pouvez aussi consulter la liste des fonctions et types d'un module (sans documentation) à l'aide de la directive #browse, comme suit :

# #browse "List";;
module List :
  sig
    type 'a t = 'a list
    type 'a enumerable = 'a t
    type 'a mappable = 'a t
    val length : 'a list -> int
    val hd : 'a list -> 'a
    val tl : 'a list -> 'a list
    val is_empty : 'a list -> bool
    val cons : 'a -> 'a list -> 'a list
    val first : 'a list -> 'a
    val last : 'a list -> 'a
    val at : 'a list -> int -> 'a
    val rev : 'a list -> 'a list
    val append : 'a list -> 'a list -> 'a list
    val rev_append : 'a list -> 'a list -> 'a list
    val concat : 'a list list -> 'a list
    val flatten : 'a list list -> 'a list
    val make : int -> 'a -> 'a list
    val init : int -> (int -> 'a) -> 'a list
    val iter : ('a -> unit) -> 'a list -> unit
    val iteri : (int -> 'a -> 'b) -> 'a list -> unit
    val map : ('a -> 'b) -> 'a list -> 'b list
    val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list
    val rev_map : ('a -> 'b) -> 'a list -> 'b list
    val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
    val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b
    val reduce : ('a -> 'a -> 'a) -> 'a list -> 'a
    val max : 'a list -> 'a
    val min : 'a list -> 'a
    val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit
    val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
    val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
    val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a
    val fold_right2 :
      ('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c
    val for_all : ('a -> bool) -> 'a list -> bool
    val exists : ('a -> bool) -> 'a list -> bool
    val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
    val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
    val mem : 'a -> 'a list -> bool
    val memq : 'a -> 'a list -> bool
    val find : ('a -> bool) -> 'a list -> 'a
    val find_exn : ('a -> bool) -> exn -> 'a list -> 'a
    val findi : (int -> 'a -> bool) -> 'a list -> int * 'a
    val find_map : ('a -> 'b option) -> 'a list -> 'b
    val rfind : ('a -> bool) -> 'a list -> 'a
    val filter : ('a -> bool) -> 'a list -> 'a list
    val filter_map : ('a -> 'b option) -> 'a list -> 'b list
    val find_all : ('a -> bool) -> 'a list -> 'a list
    val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
    val index_of : 'a -> 'a list -> int option
    val index_ofq : 'a -> 'a list -> int option
    val rindex_of : 'a -> 'a list -> int option
    val rindex_ofq : 'a -> 'a list -> int option
    val unique : ?cmp:('a -> 'a -> bool) -> 'a list -> 'a list
    val assoc : 'a -> ('a * 'b) list -> 'b
    val assoc_inv : 'a -> ('b * 'a) list -> 'b
    val assq : 'a -> ('a * 'b) list -> 'b
    val mem_assoc : 'a -> ('a * 'b) list -> bool
    val mem_assq : 'a -> ('a * 'b) list -> bool
    val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list
    val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list
    val split_at : int -> 'a list -> 'a list * 'a list
    val split_nth : int -> 'a list -> 'a list * 'a list
    val remove : 'a list -> 'a -> 'a list
    val remove_if : ('a -> bool) -> 'a list -> 'a list
    val remove_all : 'a list -> 'a -> 'a list
    val take : int -> 'a list -> 'a list
    val drop : int -> 'a list -> 'a list
    val take_while : ('a -> bool) -> 'a list -> 'a list
    val takewhile : ('a -> bool) -> 'a list -> 'a list
    val drop_while : ('a -> bool) -> 'a list -> 'a list
    val dropwhile : ('a -> bool) -> 'a list -> 'a list
    val interleave : ?first:'a -> ?last:'a -> 'a -> 'a list -> 'a list
    val enum : 'a list -> 'a Enum.t
    val of_enum : 'a Enum.t -> 'a list
    val backwards : 'a list -> 'a Enum.t
    val of_backwards : 'a Enum.t -> 'a list
    val split : ('a * 'b) list -> 'a list * 'b list
    val combine : 'a list -> 'b list -> ('a * 'b) list
    val make_compare : ('a -> 'a -> int) -> 'a list -> 'a list -> int
    val sort : ?cmp:('a -> 'a -> int) -> 'a list -> 'a list
    val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list
    val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list
    val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
    exception Empty_list
    exception Invalid_index of int
    exception Different_list_size of string
    val t_of_sexp : (Sexplib.Sexp.t -> 'a) -> Sexplib.Sexp.t -> 'a t
    val sexp_of_t : ('a -> Sexplib.Sexp.t) -> 'a t -> Sexplib.Sexp.t
    val print :
      ?first:string ->
      ?last:string ->
      ?sep:string ->
      ('a IO.output -> 'b -> unit) ->
      'a IO.output -> 'b t -> unit
    val sprint :
      ?first:string ->
      ?last:string ->
      ?sep:string ->
      ('a IO.output -> 'b -> unit) -> 'b t -> string
    val nth : 'a list -> int -> 'a
    module Exceptionless :
      sig
        val rfind : ('a -> bool) -> 'a list -> 'a option
        val findi : (int -> 'a -> bool) -> 'a list -> (int * 'a) option
        val split_at :
          int ->
          'a list -> [ `Invalid_index of int | `Ok of 'a list * 'a list ]
        val at : 'a list -> int -> [ `Invalid_index of int | `Ok of 'a ]
        val assoc : 'a -> ('a * 'b) list -> 'b option
        val assoc_inv : 'a -> ('b * 'a) list -> 'b option
        val assq : 'a -> ('a * 'b) list -> 'b option
      end
    module Labels :
      sig
        val init : int -> f:(int -> 'a) -> 'a list
        val iter : f:('a -> unit) -> 'a list -> unit
        val iteri : f:(int -> 'a -> 'b) -> 'a list -> unit
        val map : f:('a -> 'b) -> 'a list -> 'b list
        val mapi : f:(int -> 'a -> 'b) -> 'a list -> 'b list
        val rev_map : f:('a -> 'b) -> 'a list -> 'b list
        val fold_left : f:('a -> 'b -> 'a) -> init:'a -> 'b list -> 'a
        val fold_right : f:('a -> 'b -> 'b) -> 'a list -> init:'b -> 'b
        val iter2 : f:('a -> 'b -> unit) -> 'a list -> 'b list -> unit
        val map2 : f:('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
        val rev_map2 : f:('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
        val fold_left2 :
          f:('a -> 'b -> 'c -> 'a) -> init:'a -> 'b list -> 'c list -> 'a
        val fold_right2 :
          f:('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> init:'c -> 'c
        val for_all : f:('a -> bool) -> 'a list -> bool
        val exists : f:('a -> bool) -> 'a list -> bool
        val for_all2 : f:('a -> 'b -> bool) -> 'a list -> 'b list -> bool
        val exists2 : f:('a -> 'b -> bool) -> 'a list -> 'b list -> bool
        val find : f:('a -> bool) -> 'a list -> 'a
        val find_exn : f:('a -> bool) -> exn -> 'a list -> 'a
        val findi : f:(int -> 'a -> bool) -> 'a list -> int * 'a
        val rfind : f:('a -> bool) -> 'a list -> 'a
        val filter : f:('a -> bool) -> 'a list -> 'a list
        val filter_map : f:('a -> 'b option) -> 'a list -> 'b list
        val find_all : f:('a -> bool) -> 'a list -> 'a list
        val partition : f:('a -> bool) -> 'a list -> 'a list * 'a list
        val remove_if : f:('a -> bool) -> 'a list -> 'a list
        val take_while : f:('a -> bool) -> 'a list -> 'a list
        val drop_while : f:('a -> bool) -> 'a list -> 'a list
        val stable_sort : ?cmp:('a -> 'a -> int) -> 'a list -> 'a list
        val fast_sort : ?cmp:('a -> 'a -> int) -> 'a list -> 'a list
        val merge : cmp:('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
      end

Vocabulaire L'information qui vient d'être affichée sur le module List et qui commence par sig et s'achève par end s'appelle la signature du module. La notion de signature généralise aux modules la notion de type d'une valeur.

Vous constaterez que certaines fonctions du module List font appel à des types d'autres modules Enum et IO. Nous nous intéresserons à ces modules dans des chapitres ultérieurs. Vous constaterez aussi que le module List contient deux sous-modules Labels et Exceptionless, dont nous parlerons plus tard. Pour le moment, il suffit de savoir qu'un module peut contenir un sous-module. Comme pour les valeurs et les types, le nom complet d'un sous-module est le nom du module qui le contient, suivi d'un point, suivi du nom local du sous-module. Ainsi, le module Labels que nous avons sous les yeux a pour nom complet List.Labels et le module Exceptionless aura pour nom complet List.Exceptionless.


Ouverture de module

modifier

Considérons la fonction suivante :

# let second_half l =
    let l' = List.unique l in
    List.take (List.length l' / 2) l';;
val second_half : 'a list -> 'a list = <fun>

Cette fonction prend en argument une list l, en extrait la liste l' des valeurs uniques de l (c'est-à-dire supprime les doublons), puis calcule la longueur de l', la divise par deux et se sert de cette valeur pour renvoyer la première moitié de la liste l'. Si cette fonction n'est probablement guère utile en pratique, nous pouvons constater qu'elle fait appel trois fois au préfixe List., peut-être aux dépens de la lisibilité.

Il serait certainement plus agréable de n'avoir à préciser qu'une seule fois que nous manipulons des fonctions du module List. C'est à cela que sert la commande open. Nous l'utiliserons comme suit :

# open List;;
# let second_half l =  
    let l' = unique l in
    take (length l' / 2) l';;
val second_half : 'a list -> 'a list = <fun>

Ou encore

# let second_half l =  
    open List in
      let l' = unique l in
      take (length l' / 2) l';;
val second_half : 'a list -> 'a list = <fun>

Dans ces deux extraits, nous n'avons eu à mentionner List qu'une seule fois. Respectivement, ces extraits sont équivalents à :

# type 'a t  = 'a List.t
  let unique = List.unique
  let take   = List.take
  let length = List.length;;
  (* ... *)
# let second_half l =  
    let l' = unique l in
    take (length l' / 2) l';;
val second_half : 'a list -> 'a list = <fun>

ou à

# let second_half l =  
    let unique = List.unique
    and take   = List.take
    and length = List.length
    (* ... *)
    in
      let l' = unique l in
      take (length l' / 2) l';;
val second_half : 'a list -> 'a list = <fun>

Dans tous les cas, la fonction second_half obtenue est identique à celle que nous avions initialement. La seule différence est la notation plus légère.

Vocabulaire L'opération open List est l'ouverture du module List. L'opération open List in (*...*) est l'ouverture locale du module List.

Pour des raisons de confort de notation, il est aussi possible d'ouvrir plusieurs modules en une seule opération. Ainsi, pour ouvrir le module List et le module String, on pourra écrire

# open List, String;;

On utilisera fréquemment ce mécanisme pour ouvrir en une opération le module List et son sous-module Labels, qui redéfinit certaines opérations de List pour permettre d'employer des notations plus robustes (mais plus lourdes), comme suit :

# open List, Labels;;

Nous reviendrons dans un chapitre ultérieur sur le rôle exact de ce module Labels.

Le module Standard

modifier

Au cours des chapitres précédents, nous avons manipulé un certain nombre de fonctions sans préciser de quel module elles sont tirées : print_endline, int_of_string, float_of_int etc. Ces fonctions et bien d'autres viennent d'un module spécial nommé Standard, qui est automatiquement ouvert. Ce module définit aussi de nombreuses fonctions de gestion des erreurs, d'affichage, de manipulation de fichiers, de calculs sur les entiers ou les nombres flottants, etc.

Comme pour les autres modules, vous pouvez consulter le contenu du module Standard à l'aide de #browse ou #man_module.

Précautions et limitations

modifier
Majuscules
modifier

Insistons sur le fait qu'un nom de module commence toujours par une majuscule. Ainsi, si vous écrivez List.length, OCaml sait qu'il doit chercher la fonction length du module List et, si nécessaire, préalablement charger ce module. À l'inverse, si vous écrivez list.length, OCaml sait qu'il doit chercher le champ length d'un enregistrement nommé list, ce qui est une opération beaucoup plus simple.

En particulier, les deux constructions produiront des messages différents :

# List.longueur;;
  ^^^^^^^^^^^^^
Error: Unbound value List.longueur
# Liste.longueur;;
  ^^^^^^^^^^^^^^
Error: Unbound value Liste.longueur
# liste.longueur;;
  ^^^^^
Error: Unbound value liste
Constructeurs et modules
modifier

De la même manière qu'un nom de valeur ou de type, un nom de constructeur doit être préfixé par le nom du module qui le contient.

Ainsi, le module Unix définit de nombreux constructeurs, un pour chaque erreur qui peut avoir lieu lors d'une interaction avec le système d'exploitation. Parmi ces erreurs, citons arbitrairement EBUSY, qui peut être invoquée lorsque le programme tente d'accéder à une ressource qui est déjà occupée par un autre logiciel. Comme nous pouvons le constater, le nom complet de cette ressource est Unix.EBUSY:

# EBUSY;;
  ^^^^^
Error: Unbound constructor EBUSY
# Unix.EBUSY;;
- : Batteries.Unix.error = Batteries.Unix.EBUSY

# open Unix;;

# EBUSY;;
- : Batteries.Unix.error = Batteries.Unix.EBUSY

Ceci est aussi valable pour les enregistrements. Ainsi, le module Complex définit une structure type t = {re : float; im : float}. Le nom complet des champs est en fait Complex.re et Complex.im. Pour nous en convaincre, voici un exemple

# {re = 5.0; im = 1.0};;
  ^^^^^^^^^^^^^^^^^^^^
Error: Unbound record field label re

# {Complex.re = 5.0; Complex.im = 1.0};;
- : Batteries.Complex.t =
{Batteries.Complex.re = 5.; Batteries.Complex.im = 1.}

# open Complex;;

# {re = 5.0; im = 1.0};;
- : Batteries.Complex.t = {re = 5.; im = 1.}

Nous avons vu que le module List définit un type 'a t (le type des listes contenant des éléments de type 'a, c'est-à-dire un synonyme de 'a list). De la même manière, le module Int définit un type t (le type des entiers, c'est-à-dire un synonyme de int), le module Rope définit un type t (le type des textes contenant des caractères internationaux au standard Unicode), etc. De fait, une très grande partie des modules de OCaml Batteries Included définissent un type t. Il s'agit d'une convention de nommage simple : si un module est conçu spécialement pour accueillir une structure de données, le nom du type correspondant est t.

Afin d'éviter de confondre tous ces types qui portent le même nom, on prendra l'habitude d'utiliser le nom complet de chaque type, donc Rope.t, etc.

Batteries
modifier

Revenons sur un de nos exemples :

# type associations = (string * int) List.t;;
type associations = (string * int) Batteries.List.t

Comme nous pouvons le constater, OCaml a accepté cette définition mais la réponse mentionne Batteries.List.t et non List.t. Nous laissons au lecteur le soin de vérifier que ces deux types sont bien identiques.

À quoi est du ce renommage ?

De fait, le nom complet du module List est Batteries.List, ce qui signifie que le module List fait partie du module Batteries. Ce dernier est un module spécial, qui contient tous les autres modules de OCaml Batteries Included, y compris Standard et dont vous n'avez jamais besoin de taper le nom. Nous aurions cependant tout aussi bien pu taper :

# type associations = (string * int) Batteries.List.t;;
type associations = (string * int) Batteries.List.t

En général, nous éviterons de mentionner explicitement Batteries, pour de simples raisons de lisibilité.

Ouverture et écrasement
modifier

Comme nous l'avons mentionné plus haut, l'ouverture d'un module définit implicitement de nombreuses valeurs. Ainsi, nous aurons

# let length = 6;;
val length : int = 6
# length;;
- : int = 6
# open List;;
# length;;
- : 'a list -> int = <fun>

Dans cet extrait, la constante length a été écrasée par List.length lors de l'ouverture. Si ce comportement est parfois désiré, il s'agit aussi d'une source d'erreurs assez fréquente. Pour éviter tout accident, on préférera fréquemment employer l'ouverture locale, moins fragile puisqu'elle ne déborde pas d'un domaine visible au premier coup d'œil.

Exercice

modifier
  1. Formulez une expérience pour permettre de vérifier que le type 'a List.t est bien le même que le type 'a Batteries.List.t.
  2. Écrivez une fonction d'une seule ligne qui renvoie le deuxième élément d'une liste. Pour ce faire, vous utiliserez uniquement les fonctions List.hd et List.tl.

Créer de nouveaux modules

modifier

Bien entendu, il est possible d'écrire de nouveaux modules et de les utiliser comme les modules d'OCaml Batteries Included. Ceci se fait en deux étapes : définir le corps du module (c'est-à-dire les fonctionnalités du module) puis, si nécessaire, sa signature (c'est-à-dire les restrictions sur l'usage du module).

Commençons par nous intéresser au corps du module.


Créer un nouveau module peut se faire aussi simplement que d'écrire un programme complet. De fait, la seule différence entre un programme complet et un module est la manière de le compiler. Ainsi, pour créer un module on commencera par sauvegarder du code source dans un fichier d'extension .ml, que l'on compilera vers un fichier d'extension .cmo (pour obtenir un module portable non-optimisé) ou .cmx (pour obtenir un module portable optimisé). De plus, si vous ne souhaitez utiliser votre module que dans un programme compilé, vous n'avez pas besoin de le compiler du tout, OCamlBuild s'en chargera pour vous.

Ainsi, si nous souhaitons placer dans un module les fonctions que nous avons définies précédemment sur les couleurs de cartes couleur.ml. Comme ce module servira uniquement à accueillir une structure de données qui permettra de manipuler les couleurs de cartes, profitons-en pour renommer notre type de couleur_de_carte en t. Pour respecter les conventions de nommage habituelles des modules, nous en profiterons pour renommer quelques fonctions.

type t = 
   | Trefle
   | Pique
   | Coeur
   | Carreau ;;

let to_string = function
   | Trefle -> "trefle"
   | Pique  -> "pique"
   | Coeur  -> "coeur"
   | Carreau-> "carreau" ;;

De la même manière, nous pouvons définir un nouveau module qui contiendra les fonctions de manipulation des cartes elles-mêmes. Appelons le fichier cartes.ml. Ce module utilisera le module Couleur, défini à l'aide du fichier précédent. Profitons-en de même pour renommer t notre type de cartes.

type t = 
  | Atout    of int
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of int * Couleur.t ;;

let string_of_chiffre_francais = function
   | 1 -> "Un"
   | 2 -> "Deux"
   | 3 -> "Trois"
   | 4 -> "Quatre"
   | 5 -> "Cinq"
   | 6 -> "Six"
   | 7 -> "Sept"
   | 8 -> "Huit"
   | 9 -> "Neuf"
   | 0 -> "Zero" 
   | n -> string_of_int n ;;

let to_string carte = 
  open Couleur in match carte with
  | Atout    i    -> string_of_int i ^ " d'atout"
  | Roi      c    -> "Roi de "       ^ ( to_string c ) (*Notre définition de to_string utilise Couleur.to_string*)
  | Dame     c    -> "Dame de "      ^ ( to_string c )
  | Cavalier c    -> "Cavalier de "  ^ ( to_string c )
  | Valet    c    -> "Valet de "     ^ ( to_string c )
  | As       c    -> "As de "        ^ ( to_string c )
  | Nombre (i, c) -> string_of_chiffre_francais i ^ " de " ^ ( to_string c ) ;;

let get_couleur = function
    | As       c -> Some c
    | Roi      c -> Some c
    | Valet    c -> Some c
    | Dame     c -> Some c
    | Cavalier c -> Some c
    | Nombre _ c -> Some c
    | _          -> None ;;

Si nous le souhaitons, nous pouvons maintenant compiler nos modules :

$ ocamlfind batteries/ocamlbuild couleur.cmo carte.cmo
Finished, 4 targets (0 cached) in 00:00:02.

Note L'ordre couleur.cmo carte.cmo est sans importance. Si un module dépend d'un autre, OCamlBuild compilera les modules dans l'ordre de dépendances.

Rappelons que nous n'avons pas besoin de compiler les modules nous-mêmes, à moins d'avoir envie de nous en servir en mode interactif, car OCamlBuild les compilera lui-même si nécessaire.

Testons maintenant nos modules dans un programme :

(*Fichier test.ml*)

let premiere_carte_qui_porte_malheur = Carte.As Couleur.Pique;;
let deuxieme_carte_qui_porte_malheur = Carte.Atout 16;;

print_endline ("La première carte qui porte malheur est "^Carte.to_string premiere_carte_qui_porte_malheur);;
print_endline ("La deuxième carte qui porte malheur est "^Carte.to_string deuxieme_carte_qui_porte_malheur);;

Compilons et testons :

$ ocamlfind batteries/ocamlbuild test.byte
Finished, 7 targets (4 cached) in 00:00:01.
$ ./test.byte
La première carte qui porte malheur est As de pique
La deuxième carte qui porte malheur est 16 d'atout

Charger un module

modifier

Une fois qu'un fichier .ml a été compilé sous la forme d'un .cmo, nous pouvons le charger dans le mode interactif. Pour ce faire, nous utiliserons les directives #cd (pour aller dans le répertoire qui contient le fichier .cmo) et #load (pour charger effectivement le fichier .cmo). Si un .cmo dépend d'un autre .cmo, les fichiers doivent être chargés par ordre de dépendance.

Par défaut, OCamlBuild sauvegarde les fichiers .cmo dans un répertoire nommé _build. Nous pouvons donc faire

(*Nous n'avons pas encore chargé le module Carte*)
# Atout 16;;
  ^^^^^
Error: Unbound constructor Atout

# Carte.Atout 16;;
  ^^^^^^^^^^^
Error: Unbound constructor Carte.Atout

(*Chargeons les deux modules*)
# #cd "_build";;

# #load "couleur.cmo";;

# #load "carte.cmo";;

# Atout 16;;
  ^^^^^
Error: Unbound constructor Atout

# Carte.Atout 16;;
- : Carte.t = Carte.Atout 16

Comme l'indique ce dernier extrait, charger un fichier .cmo n'ouvre pas automatiquement le module. Il est donc possible de charger un module sans crainte d'écraser les noms de fonctions ou de types existants.

Une autre manière d'ouvrir un ou plusieurs fichiers .cmo est de donner la liste sur la ligne de commande

$ ocamlfind batteries/ocaml couleur.cmo carte.cmo
#        Objective Caml version 3.11.0

      _________________________________
     |       | |                       |
    [| +     | | Batteries Included  - |
     |_______|_|_______________________|
      _________________________________
     |                       | |       |
     | -    Type '#help;;'   | |     + |]
     |_______________________|_|_______|


# Carte.Atout 16;;
- : Carte.t = Carte.Atout 16

Vous disposez maintenant de toutes les connaissances nécessaires pour créer et utiliser un module. Dans la section suivante, nous allons voir comment et pourquoi restreindre l'accès à un module.

Vocabulaire Indépendamment du langage de programmation, le fait de concevoir un programme comme un ensemble de modules dotés de dépendances simples et précises s'appelle la modularité.

Précautions et limitations

modifier
Un programme est un module
modifier

Insistons sur un point : un module peut contenir exactement le même genre de choses qu'un programme. Le fichier suivant, sauvegardé sous le nom bonjour.ml peut donc être compilé comme un module tout à fait acceptable :

print_endline "Bonjour, le monde !";;

Il s'agit d'un module qui ne définit aucune fonction, aucun type, mais qui, lorsqu'il est utilisé par un programme, affiche Bonjour, le monde.

Pour nous en convaincre, commençons par le compiler :

$ ocamlfind batteries/ocamlbuild bonjour.cmo
Finished, 3 targets (0 cached) in 00:00:00.

Puis chargeons ce fichier en mode interactif

# #cd "_build";;
# #load "bonjour.cmo";;
Bonjour, le monde !
#
Ordre de chargement
modifier

Insistons sur un point important : si les fichiers .cmo peuvent être compilés dans n'importe quel ordre, ils doivent être chargés par ordre de dépendance, c'est-à-dire en ne chargeant que des fichiers .cmo qui dépendent de modules déjà chargés.

Ainsi, l'extrait suivant charge les modules dans le bon sens :

# #cd "_build";;
# #load "couleur.cmo";;
# #load "carte.cmo";;

Par contre, si nous essayons de charger carte.cmo avant couleur.cmo, nous obtiendrons :

# #cd "_build";;
# #load "carte.cmo";;
Error: Reference to undefined global `Couleur'

Ce choix de conception du langage permet de garantir qu'un programme qui se charge peut aussi s'exécuter. Par contraste, des langages tels que Python offrent plus de souplesse au niveau du chargement mais un programme une fois lancé peut encore s'arrêter en catastrophe lorsque des modules s'avèrent manquants.

Dépendances cycliques
modifier

Une conséquence directe de cette contrainte sur l'ordre de chargement est qu'il n'est pas possible, en OCaml, d'écrire deux modules qui dépendent chacun de l'autre. Ainsi, il est impossible de compiler les deux fichiers suivants ensemble :

(*Fichier a.ml*)
let a = 5;;
let b = B.b;;
(*Fichier b.ml*)
let a = A.a;;
let b = 6;;


Si nous essayons de compiler l'un ou l'autre, nous obtiendrons

$ ocamlfind batteries/ocamlbuild a.cmo
Circular build detected (b.cmi already seen in [ a.cmi; b.cmi; a.cmo ])
Compilation unsuccessful after building 2 targets (0 cached) in 00:00:00.

Vocabulaire On appelle ce type de problème une dépendance cyclique.

Contrairement à ce qui est autorisé en Java ou en Python, il est impossible d'écrire un programme ou un module contenant des dépendances cycliques en OCaml. Cette limitation est liée directement à la contrainte sur l'ordre de chargement et au fait qu'un module peut contenir du code qui sera exécuté lors de son chargement. Si un programme contenait deux modules A et B mutuellement dépendants, ni le code de l'un ni le code de l'autre ne pourrait s'exécuter lors du chargement, puisque le chargement de chacun des deux nécessiterait le chargement préalable de l'autre.

Il est généralement admis en informatique qu'une dépendance cyclique entre deux sous-ensembles distincts d'un programme est une erreur de conception. Il existe une plusieurs techniques pour transformer un projet contenant des dépendances cycliques de manière à supprimer ces dépendances. La technique la plus simple, qui n'est pas toujours envisageable ou satisfaisante, consiste à isoler les fonctionnalités qui introduisent ces dépendances cycliques et à les rassembler dans un module propre, puis à masquer cette technique d'implantation à l'aide de la signature appropriée.

Une autre technique consiste à introduire de la récursivité mutuelle entre modules à l'aide d'un opérateur de point fixe sur module. Nous verrons cette technique bien plus tard, lors du chapitre consacré aux usages avancés des modules.

Exercices

modifier
  1. Définissez un module Ast, qui regroupera les types programme, instruction... et les fonctions string_of_programme, string_of_instruction définis au chapitre précédent.

Contraindre l'utilisation d'un module

modifier

Dans ce qui précède, nous avons vu comment créer des modules. Si ceci est déjà très utile, OCaml propose quelques concepts supplémentaires qui serviront notamment à documenter l'utilisation d'un module ou à la restreindre l'utilisation d'un module. Cette notion de restriction sur l'utilisation a de nombreux usages. On s'en servira d'une part pour garantir au développeur du module que des changements dans le fonctionnement interne du module n'interfèreront pas avec le fonctionnement des programmes qui utilisent ce module, et d'autre part pour garantir que l'utilisateur ne peut pas, par accident, introduire des valeurs incorrectes ou contraires aux hypothèses, telles qu'un 25 d'Atout dans notre jeu de tarot.

Pour introduire ces restrictions, on emploiera des signatures (ou interfaces).

Signatures

modifier

Un peu plus tôt dans ce chapitre, nous avons rencontré la signature du module List. À l'aide de la directive #browse, nous avions demandé à OCaml de nous afficher la liste des fonctions et types de ce module. Une signature n'est rien d'autre qu'une telle liste, sauvegardée dans un fichier d'extension .mli . Si nous désirons compiler une signature, ce qui n'est jamais nécessaire mais peut être utile le temps d'un test, il nous suffit de demander à OCamlBuild de produire un fichier d'extension .cmi

Ainsi, pour notre exemple du Tarot, nous allons commencer par écrire le fichier couleur.mli, qui accompagnera le fichier couleur.ml.

Note OCaml cherchera toujours la signature correspondant à un fichier toto.ml dans le fichier correspondant toto.mli.

type t =
   | Trefle
   | Pique
   | Coeur
   | Carreau ;;

val to_string : t -> string ;;

De la même manière, nous pouvons écrire le fichier carte.mli, qui accompagnera le fichier carte.ml :

type t =
  | Atout    of int
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of int * Couleur.t ;;

val string_of_chiffre_francais : int -> string ;;

val to_string : t -> string ;;

val get_couleur : t -> t option ;;

À ce point, nous pourrions nous arrêter : nous disposons d'une interface complète pour chacun de nos deux modules, interface qui ne restreint rien. De fait, accordons-nous une petite pause, le temps de tester que tout ceci compile correctement :

$ ocamlfind batteries/ocamlbuild test.byte
Finished, 11 targets (3 cached) in 00:00:01.

Mais nous pouvons faire mieux.

Commençons par cette fonction string_of_chiffre_francais. Elle est certes nécessaire pour écrire la fonction to_string mais sa présence est plutôt embarrassante dans un module consacré à des cartes de tarot. Faisons-la donc simplement disparaître.

Voici donc une nouvelle version de carte.mli :

type t =
  | Atout    of int
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of int * Couleur.t ;;

val to_string : t -> string ;;

val get_couleur : t -> Couleur.t option ;;

Comme nous pouvons le constater, cette fonction n'était effectivement pas nécessaire pour les utilisateurs du module Carte :

$ ocamlfind batteries/ocamlbuild test.byte
Finished, 11 targets (6 cached) in 00:00:01.

Maintenant que nous l'avons supprimée de l'interface du module, cette fonction ne peut plus être manipulée par les utilisateurs de ce module :

# #cd "_build" ;;

# #load "couleur.cmo" ;;

# #load "carte.cmo" ;;

# Carte.to_string ;;
- : Carte.t -> string = <fun>

# Carte.string_of_chiffre_francais ;;
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: unbound value Carte.string_of_chiffre_francais

Lorsque nous avons supprimé la fonction de l'interface du module, nous avons interdit à tout utilisateur de la manipuler. Par contre, cette fonction est toujours présente dans le module, car elle est nécessaire à Carte.to_string, fonction dont nous venons de vérifier qu'elle est toujours présente.

Vocabulaire La possibilité de cacher certaines parties d'un module pour empêcher leur utilisation à l'extérieur du module est une forme limitée d'encapsulation.

Maintenant que nous avons légèrement nettoyé l'interface, intéressons-nous un peu plus au cas des Atouts : un Atout est une carte dont le numéro est compris entre 1 et 22 -- ou entre 0 et 21, le choix doit être fait lors de la conception du module. À l'heure actuelle, rien ne m'interdit d'introduire, par accident, une valeur Atout 25. C'est malheureux et cela peut provoquer des bugs. Fort heureusement, nous pouvons corriger ce problème.

Pour ce faire, nous allons commencer par modifier l'implantation pour faire en sorte que le constructeur Atout n'utilise pas des entiers mais des valeurs d'un nouveau type, que nous allons appeler atout :

(*Fichier carte.ml*)
type t =
  | Atout    of atout
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of int * Couleur.t ;;

Bien entendu, pour que ceci compile, il faut que nous ayons préalablement défini le type atout. Comme nous souhaitons que les valeurs d'atouts soient bien des entiers, nous allons faire de atout un synonyme de int -- pour le moment.

(*Fichier carte.ml*)
type atout = int;;

Qu'avons-nous gagné pour le moment ? En apparence rien. En fait, l'intérêt de cette manipulation est que, si nous avons bien défini que le type atout est un synonyme de int, nous ne sommes pas obligés de rendre cette information publique. Ainsi, dans carte.mli, nous allons écrire, sans plus de détails :

(*Fichier carte.mli*)
type atout;;

type t =
  | Atout    of atout
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of int * Couleur.t ;;

Vocabulaire Un type dont le nom est donné dans l'interface mais dont la définition complète reste cachée est appelé un type abstrait.

En rendant le type atout abstrait, nous avons interdit à l'utilisateur de fabriquer lui-même des valeurs de ce type. Il nous reste à définir deux fonctions triviales qui permettront de transformer un atout en int, ce qui peut servir pour comparer des valeurs d'atouts, ou pour transformer un entier compris entre 1 et 22 en atout :

(*Fichier carte.ml*)

let int_of_atout x = x;;

let atout_of_int x =
 if 1 <= x && x <= 22 then Some x
 else                      None;;

Quel type donner à ces fonctions ? La fonction int_of_atout pourrait avoir pour type 'a -> 'a, tandis que atout_of_int pourrait avoir pour type int -> int option. Mais, comme nous avons en tête l'idée que la première de ces fonctions sert à transformer un atout en nombre entier et que la deuxième de ces fonctions sert à transformer un nombre entier en atout, nous allons leur donner les types suivants :

(*Fichier carte.mli*)

val int_of_atout : atout -> int ;;

val atout_of_int : int -> atout option ;;

Nous disposons maintenant de deux fonctions qui permettent de manipuler les valeurs de type atout. Tant que nous n'ajoutons pas d'autre fonction qui renvoie une valeur de type atout, nous pouvons être certains que, <emph>par construction et malgré toute la mauvaise volonté du monde, aucun utilisateur n'arrivera à avoir un atout dont la valeur n'est pas comprise entre 1 et 22</emph>.

Vocabulaire La valeur d'un élément de type atout est dite sure par construction, puis qu'il n'existe aucune manière pour un utilisateur de violer sa propriété fondamentale, qui est d'être comprise entre 1 et 22.

Note En programmation fonctionnelle, on considère comme primordial cet question de sûreté par construction. L'essentiel du temps passé à concevoir une bibliothèque de programmation fonctionnelle est généralement consacré à essayer d'obtenir le plus de garanties de sûreté.

Note Cette question de sûreté par construction est un très proche parent des capacités par objets présentes dans quelques langages de programmation conçus pour la sûreté (dont une variante de OCaml).

Vocabulaire Profitons-en pour préciser que, indépendamment du langage de programmation, une bibliothèque est dite sûre s'il n'y a pas moyen de causer de comportements imprévisibles à l'aide de cette bibliothèque.

Vocabulaire Un langage est dit sûr s'il est possible de programmer des bibliothèques sûres. On considère généralement que le langage C et les langages dynamiques ne sont pas sûrs, puisqu'il y a moyen de détourner totalement le fonctionnement d'une bibliothèque pour lui faire adopter un comportement imprévisible. Java, C#, OCaml, Haskell ou Coq seront des langages beaucoup plus sûrs. Entre eux, on considère généralement Java comme moins sûr que C#, lui-même moins sûr que OCaml, lui-même moins sûr que Haskell, lui-même moins sûr que Coq.

Nous pouvons encore améliorer légèrement les choses en autorisant l'utilisateur à utiliser soit 0 soit 22 pour l'excuse (carte qui, rappelons-le, peut avoir deux valeurs distinctes) mais en nous débrouillant pouru que les valeurs employées en internes soient toujours comprises entre 1 et 22. Pour ce faire, il nous suffit de remplacer la définition de atout_of_int par

(*Fichier carte.ml*)
let atout_of_int x =
 if x = 0 then                  Some 22
 else if 1 <= x && x <= 22 then Some x
 else                           None;;

De la même manière que nous avons introduit un type atout et des fonctions atout_of_int et int_of_atout pour les atouts dont la valeur doit être comprise entre 1 et 22 (ou 0 et 22), nous allons introduire un type nombre et des fonctions nombre_of_int et int_of_nombre pour les cartes numérotées, dont la valeur doit être comprise entre 2 et 10.

(*Fichier carte.mli*)
type nombre;;

type t =
  | Atout    of atout
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of nombre * Couleur.t ;;

val nombre_of_int : int -> nombre option ;;

val int_of_nombre : nombre -> int ;;
(*Fichier carte.ml*)
type nombre = int;;

type t =
  | Atout    of atout
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of nombre * Couleur.t ;;

let int_of_nombre x = x;;

let nombre_of_int x =
  if 2 <= x && x <= 10 then Some x
  else                      None;;

Note Il n'est pas nécessaire de déclarer les types et valeurs dans le même ordre dans le fichier .ml et dans le fichier .mli correspondant.

Et voilà !

Documentation

modifier

L'habitude veut que tous les fichiers .mli soient documentés à l'aide de commentaires écrits selon quelques conventions simples. Ces commentaires pourront plus tard être extraits et transformés en documentation HTML ou PDF. Pour le moment, contentons-nous de quelques annotations, que nous complèterons un peu plus tard lorsque nous aurons introduit les outils d'extraction.

Commençons par le fichier couleur.mli :

(**
 Gestion des couleurs de cartes à jouer.
*)

(**La couleur d'une carte.
   Chaque carte hormis les Atouts est doté d'une couleur de carte.*)
type t =
   | Trefle
   | Pique
   | Coeur
   | Carreau ;;

val to_string : t -> string ;;
(**Renvoie une représentation de la couleur de la carte, sous la forme d'un texte en français*)

Puis le fichier carte.mli :

(*Fichier carte.mli*)

type atout (**La valeur d'un atout. 
              Cette valeur peut être transformée en un entier compris entre 1 et 22. L'Excuse vaut donc 22*)

val int_of_atout : atout -> int
(**Renvoie le nombre correspondant à la valeur d'un Atout.
   Ce nombre est un entier compris entre 1 inclus et 22 inclus. L'Excuse vaut 22.*)

val atout_of_int : int -> atout option
(**Convertit un nombre en l'Atout correspondant.
   Le nombre doit être un entier compris entre 0 et 22 inclus. Les valeurs 0 et 22 représentent toutes deux l'Excuse.
   Cette fonction renvoie None si le nombre n'est pas compris entre 0 et 22.*)

type nombre (**La valeur d'une carte numérotée.
              Cette valeur peut être transformée en un entier compris entre 2 et 10. L'As est géré comme une figure.*)

val int_of_nombre : nombre -> int
(**Renvoie le nombre correspondant à la valeur d'une carte numérotée.
   Ce nombre est un entier compris entre 2 inclus et 10 inclus.*)

val nombre_of_int : int -> nombre option
(**Convertit un entier en la valeur de la carte numérotée correspondante.
   Le nombre doit être un entier compris entre 2 et 10 inclus.
   Cette fonction renvoie None si le nombre n'est pas compris entre 2 et 10 inclus.*)

(** Une carte de Tarot*)
type t =
  | Atout    of atout
  | Roi      of Couleur.t
  | Dame     of Couleur.t
  | Cavalier of Couleur.t
  | Valet    of Couleur.t
  | As       of Couleur.t
  | Nombre   of nombre * Couleur.t (**Une carte numérotée (l'As est considéré comme une figure)*);;

val to_string : t -> string ;;
(**Renvoie une représentation e la carte sous la forme d'un texte en français.*)

val get_couleur : t -> Couleur.t option ;;
(**Renvoie la couleur d'une carte. Si la carte est un atout, elle n'a pas de couleur.*)

Types privés

modifier

Nous venons de voir la notion de types abstraits, qui nous a permis de garantir que les cartes numérotées et les atouts étaient forcément des valeurs valides. Le mécanisme consiste à masquer toute information sur la manière dont est implanté un type, et à ne fournir que des fonctions sûres pour la manipulation des valeurs de ce type.

Dans certains cas, cela semble un peu exagéré. On pourrait préférer laisser publique l'information sur la manière dont est implanté un type et de se contenter de restreindre la possibilité de créer des valeurs de ce type. Ainsi, dans notre exemple en cours, la seule manière d'utiliser une valeur de type atout ou nombre est de commencer par la convertir en entier à l'aide de int_of_atout ou int_of_nombre. Nous gagnerions en simplicité (et peut-être en vitesse) si nous pouvions autoriser un programmeur à utiliser toute valeur de type nombre ou atout directement comme un int, à condition de disposer d'un mécanisme pour protéger la création des valeurs de type nombre ou atout.

C'est le principe des types privés.

Ainsi, dans carte.mli, nous pouvons remplacer

type atout (**La valeur d'un atout.
              Cette valeur peut être transformée en un entier compris entre 1 et 22. L'Excuse vaut donc 22*)

val int_of_atout : atout -> int
(**Renvoie le nombre correspondant à la valeur d'un Atout.
   Ce nombre est un entier compris entre 1 inclus et 22 inclus. L'Excuse vaut 22.*)

type nombre (**La valeur d'une carte numérotée.
              Cette valeur peut être transformée en un entier compris entre 2 et 10.
              L'As n'est pas représenté comme un nombre mais comme une figure.*)
 
val int_of_nombre : nombre -> int
(**Renvoie le nombre correspondant à la valeur d'une carte numérotée.
   Ce nombre est un entier compris entre 2 inclus et 10 inclus.*)

par

type atout = private int
 (**La valeur d'un atout.
    Il s'agit d'un entier compris entre 1 et 22. L'Excuse vaut donc 22.*)

type nombre = private int
  (**La valeur d'une carte numérotée.
     Il s'agit d'un entier compris entre 2 et 10.
     L'As n'est pas représenté comme un nombre mais comme une figure.*)

Dans le fichier carte.ml, nous pouvons de même nous débarrasser de int_of_nombre et int_of_atout, devenus inutiles.

Pour nous convaincre que ceci fonctionne correctement, commençons par compiler le tout :

$ ocamlfind batteries/ocamlbuild carte.cmo couleur.cmo
Finished, 6 targets (2 cached) in 00:00:00.
$ ocamlfind batteries/ocaml

Puis chargeons et testons dans le mode interactif :

# #cd "_build";;
# #load "couleur.cmo";;
# #load "carte.cmo";;

(*Un atout s'affiche comme un nombre.*)
# Carte.atout_of_int 5;;
- : Carte.atout option = Some 5

# Carte.atout_of_int 0;;
- : Carte.atout option = None


(*Mais un nombre n'est pas un atout.*)
# Carte.Atout 5;;
              ^
Error: This expression has type int but is here used with type
         Carte.atout = Carte.atout

Et voilà.

À titre de comparaison

modifier
Types abstraits
modifier

La notion de type abstrait apparait, sous une forme légèrement différente, dans la majorité des langages de programmation orientée objet statiquement typés.

Ainsi, nous pouvons considérer qu'un type abstrait est un type tel que :

  • il est impossible de construire directement une valeur de ce type
  • il est impossible de se servir d'informations sur la manière dont le type est effectivement implanté.

Dans un langage à objets, ces caractéristiques se traduiront par :

  • un type abstrait est la donnée d'une interface d'objets
  • la ou les classes qui implantent cette interface sont protégées.

En Java, pour implanter la première version de notre type nombre, nous écrirons donc :

//Fichier Nombre.java
public interface Nombre
{
   public int getInt();
}

//Fichier NombreImpl.java
protected class NombreImpl implements Nombre
{
  private final int valeur;
  protected NombreImpl(int valeur)
  {
     this.valeur = valeur;
  }
  public int getInt()
  {
    return this.valeur;
  }
}

En plus de ceci, de la même manière que nous avons fourni une fonction nombre_of_int en OCaml, nous devons fournir une fonction ou une méthode usine en Java, telle que :

//Fichier NombreFactory.java
public class NombreFactory
{
  public static Nombre makeNombre(int valeur) throws Exception
  {
    if(2 <= valeur && valeur <= 10)
      return new NombreImpl(valeur);
    else
      throw new Exception();
  }
}

La version Java est plus verbeuse et prête plus à confusion. En contrepartie, elle permet plus d'extensibilité et plus de modularité, puisqu'une implantation de Nombre peut être remplacée par une autre sans avoir à réécrire le code qui manipule les instances type Nombre.

Types privés
modifier

De la même manière que les types abstraits, les types privés apparaissent, sous une autre forme, dans les langages orientés objet.

Ainsi, nous pouvons considérer qu'un type privé est un type tel que :

  • il est impossible de construire directement une valeur de ce type
  • toutes les informations sont disponibles sur la manière dont le type est effectivement implanté.

Dans un langage à objets, ces caractéristiques se traduiront naturelle par une classe dotée d'un constructeur protégé. Ainsi, en Java, pour implanter la deuxième version de notre type nombre, nous écrirons

//Fichier Nombre.java
public class Nombre
{
   private final int valeur;
   protected Nombre(int valeur) throws Exception
   {
      if(2 <= valeur && valeur <= 10)
        this.valeur = valeur;
      else
        throw new Exception();
   }
}

Exercices

modifier
  1. Donnez une signature à votre module Ast.
  2. Adaptez la signature et le module de manière à ce que nombre, text et nom soient des types abstraits. Pour l'implantation, pour le moment, vous emploierez respectivement les types concrets int, string et string.
  3. Adaptez le module Ast de manière à ce que les seules valeurs de type nom soient des chaînes de caractères qui ne contiennent que des lettres (donc ni espace ni ponctuation, etc). Pour ce faire, vous pourrez utiliser les fonctions
  • Char.is_letter, qui vérifie si un caractère est bien une lettre
  • String.length, qui renvoie la longueur d'une chaîne de caractères
  • String.get, qui renvoie le nème caractère d'une chaîne.

Sous-modules et modules locaux

modifier