• Aucun résultat trouvé

1.4 Plan de la thèse

2.1.2 Types de données

Comme la plupart des langages de programmation,OCamlfournit à la fois des types prédéfinis (entiers, flottants, booléens, chaînes de caractères. . . ) et des opérations permettant de construire des types plus complexes à partir des types prédéfinis ou de types définis précédemment. Dans

cette sous-section, nous décrivons quelques opérations disponibles pour construire de nouveaux types.

Types produit Un tuple est un type permettant de regrouper un certain nombre de données,

ces données pouvant être de types différents. Ces types peuvent être utilisés lorsqu’il est néces-saire de passer simultanément plusieurs objets à une fonction, lorsqu’une fonction a besoin de renvoyer plusieurs valeurs, ou tout simplement parce que cela correspond à ce que l’on souhaite modéliser. Les points du plan constituent un bon exemple de cette dernière situation. EnOCaml, le type des points à coordonnées entières peut être déclaré de la façon suivante :

# type point = int * int;; type point = int * int

Le point dont l’abscisse est1et l’ordonnée2peut alors être noté comme suit : # (1,2);;

- : int * int = (1, 2)

Remarquons que nous nous trouvons ici dans une situation où le type inféré parOCaml, bien que correct, n’est pas forcément celui auquel nous nous attendions. Nous pouvons aider le système à inférer le « bon » type en utilisant la notation(expr : type)comme suit :

# ((1,2) : point);; - : point = (1, 2)

Et si nous souhaitions manipuler des points pondérés, dans le contexte d’une application en physique par exemple, nous pourrions définir un type représentant ces points de la façon sui-vante :

# type weightedPoint = point * float;; type weightedPoint = point * float

Ce type permet de manipuler comme une seule entité un objet qui contient à la fois un point et son poids. Il illustre aussi le fait qu’un type prodit peut rassembler des objets de types différents.

Types somme Il arrive souvent que l’on ait à manipuler des valeurs appartenant à un

en-semble fini et connu à l’avance. Un exemple de cette situation est donné par les mois de l’année. C’est pour ce genre d’exemples que les types somme sont utiles. Voici par exemple comment pourrait être défini un type représentant les différents mois d’une année :

# type month = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec;;

type month = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec

D’après cette définition de type, il y a exactement12valeurs de typemonth, à savoir Jan, Feb, . . . , Dec. Ces valeurs sont souvent appelées des constructeurs car elles permettent de

construire des valeurs du type auquel elles appartiennent. Les constructeurs peuvent apparte-nir à au plus un type, ce qui facilite l’inférence de type en présence de constructeurs. Notons enfin que les noms des constructeurs commencent nécessairement par une lettre majuscule.

La technique utilisée pour traiter des valeurs de types tels quemonthest appeléefiltrage de motif oupattern matchingen anglais. Pour illustrer cette technique, voici comment la fonction daysrenvoyant le nombre de jours dans un mois peut être définie :

let days m = match m with | Jan -> 31

| Feb -> 28 ...

| Dec -> 31

ou, de façon un peu plus concise : let days = function

| Jan -> 31 | Feb -> 28 ...

| Dec -> 31

Dans la seconde version, l’abstraction n’apparaît plus explicitement : on a en effet remplacé let days m = match m with

par

let days = function

On évite ainsi de donner un nom à l’argument que l’on souhaite examiner à l’aide du filtrage de motif. Ceci s’avère souvent pertinent, dans la mesure où il arrive fréquemment qu’on n’ait pas besoin de faire référence à la valeur que l’on filtre.

Les types somme permettent aussi d’énumérer au sein d’un même type des valeurs de types différents. Par exemple, et bien que cela puisse sembler quelque peu artificiel, voici la définition d’un typenumberqui peut contenir soit un entier, soit un flottant.

type number = | Float of float | Int of int

Ici encore, le traitement des valeurs de type numberse fait grâce au filtrage de motif. Voici comment écrire une fonctionsumpour un tel type :

# let sum = function

| (Int x, Int y) -> Int (x+y)

| (Float x, Float y) -> Float (x +. y)

| (Int x, Float y) -> Float ((float_of_int x) +. y) | (Float x, Int y) -> Float (x +. (float_of_int y)) val sum : number * number -> number = <fun>

Remarquons qu’ici, nous avons fait le choix de passer à sum ses arguments sous la forme d’une paire de nombres, comme en témoigne le typenumber * numberde l’unique argument de sum. Nous aurions tout aussi bien pu définir sum comme une fonction à deux arguments de typenumber. Il nous aurait suffit pour cela de remplacer la première ligne de la définition précédente par :

let sum n1 n2 = match (n1,n2) with

ce qui aurait conduit le systèmeOCamlà afficher le type suivant : val sum : number -> number -> number = <fun>

Ce changement n’est cependant pas très intéressant car le couple de nombres est tout de même construit. La seule chose qui change par rapport à la première version, c’est que la construction du couple se fait dans la fonction et n’est donc plus à la charge de l’appelant. Il est possible d’éviter complètement de recourir à un couple en ne procédant pas au filtrage de motif sur les deux nombres en même temps, mais le code que l’on obtient est un peu moins lisible que celui que nous avons présenté.

Pour conclure ce paragraphe sur les types unions, il nous paraît important de mentionner qu’il est possible de réunir au sein d’un même type des constructeurs sans arguments comme ceux de monthet des constructeurs avec arguments comme ceux denumber. Voici par exemple un type linequi pourrait être utilisé par une fonction de lecture depuis un fichier pour renvoyer son résultat :

type line = | Nothing

| Something of string

Une fonction de lecture depuis un fichier texte pourrait par exemple prendre en argument le nom du fichier à lire et renvoyerSomethingavec en argument la prochaine ligne non lue du fichier, etNothingsi la fin du fichier est atteinte.

L’idée que nous venons de présenter ici est généralisée par le type polymorpheoptionque nous introduirons un peu plus loin. Dans la pratique, c’est ce dernier type qui serait utilisé dans une situation comme celle que nous venons d’évoquer, plutôt qu’un typead hoc.

Enregistrements (structures) Les structures ou enregistrements fournis parOCamlsont

similaires à ceux fournis par d’autres langages de programmation. Voici par exemple une défini-tion alternative du typepointvu précédemment utilisant une structure à la place d’un produit cartésien :

# type point = { x : int; y : int };;

type point = { x : int; y : int; }

# let p = { x = 1; y = 2 };; val p : point = {x = 1; y = 2} ou à partir d’un point existant :

# let p’ = { p with y = 0 };; val p’ : point = {x = 1; y = 0}

Enfin, on accède aux champs des structures grâce à la notationstructure.champscommune à beaucoup de langages de programmation.

Types récursifs et types polymorphes Comme leur nom l’indique, les types récursifs

sont utilisés pour décrire des données dont la structure est récursive, comme des listes ou des arbres. Voici par exemple comment définir des listes d’entiers :

# type ilist = INil | ICons of int * ilist type ilist = INil | ICons of int * ilist

comme on peut le constater, cette définition est similaire à celles des types unions. Et en fait, le typeilist que nous venons de définir est un type union, mais où le type que l’on est en train de définir apparaît aussi comme argument de l’un de ses constructeurs. Par conséquent, les valeurs de types similaires à celui que nous venons de déclarer peuvent elles aussi être traitées à l’aide de fonctions procédant par filtrage de motif, la seule différence avec les fonctions vues précédemment étant que, pour manipuler des données dont le type est récursif, on peut avoir besoin de fonctions récursives. Voici par exemple une fonction calculant la longueur de listes de typeilist:

# let rec ilength = function | INil -> 0

| ICons (_,xs) -> 1 + (ilength xs);; val ilength : ilist -> int = <fun>

Mis à part le caractère_utilisé dans les motifs lorsqu’on ne souhaite pas donner de nom explicite à un argument extrait par filtrage, cette définition ne devrait poser aucun problème de lecture. Nous pouvons donc ainsi développer toute une bibliothèque de fonctions sur les listes d’entiers. Cependant, si l’on souhaite disposer d’une bibliothèque équivalente sur les listes de caractères, le seul moyen de l’obtenir sera de copier la première bibliothèque et de l’adapter, l’on devra faire ainsi pour chaque type pour lequel on voudra construire des listes d’éléments.

Le langage OCaml offre cependant une solution plus élégante à ce problème, à savoir les types polymorphes. Il s’agit de types particulièrement adaptés à la création de structures de données génériques puisqu’ils font intervenir dans leur définition des variables de types qui peuvent être par la suite instanciées par n’importe quel type. Voici la définition des listes fournie par la bibliothèque standard deOCaml:

# type ’a list = Nil | Cons of ’a * ’a list;; type ’a list = Nil | Cons of ’a * ’a list

La variable’aest une variable de type appelée à être instanciée par un type concret ultérieu-rement. La définition des listes que nous venons de donner se lit alors de la façon suivante : « étant donné un type’a, une liste d’éléments de type ’aest soit une liste vide (constructeur Nil), soit une liste non vide (constructeurCons) constituée d’un élément de type ’a(premier argument deCons, appelétêtede la liste) et d’une autre liste d’éléments de type’a(deuxième argument deCons, appeléqueuede la liste) ». Un typeilist2équivalent au typeilistdéfinit précédemment peut alors être obtenu en remplaçant’aparintdans le type que nous venons de définir :

# type ilist2 = int list;; type ilist2 = int list

Bien que ce genre de définition soit possible, on n’y recourt pas en général, car on en a en général pas besoin. En outre, pour faciliter l’utilisation des listes,OCamlfournit les notations abrégées suivantes :

Nils’écrit[];

Cons (x,l)s’écritx::l;

Cons (x,Cons (y, Nil))s’écrit[x;y].

Comme autre exemple de type polymorphe, citons le type’a optionqui généralise le type lineintroduit précédemment. Ce type permet, comme son nom l’indique, de représenter une valeur optionnelle. Voici sa définition :

# type ’a option = None | Some of ’a;; type ’a option = None | Some of ’a

Remarquons que ce type est polymorphe mais non récursif.

Comme le montre cette présentation des mécanismes offerts parOCamlpour définir des types, ce langage est particulièrement bien adapté à la modélisation de définitions mathématiques ré-cursives et de structures de données. Comme nous le verrons par la suite, il est extrêmement aisé et naturel enCamlde définir des types pour représenter desλ-termes, des arbres ou des lexiques, types dont nous ferons une utilisation abondante par la suite. Que l’on compare cette facilité de modélisation aux possibilités offertes enProlog, et l’on comprendra aisément pourquoi nous pensons queOCamlest un langage bien plus adapté quePrologau développement d’outils tels queNessie.