Cours 4

Htmligure
Version & licenses
Creative Commons License

Types produits

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Types produits

Il existe deux façons de définir de nouvelles représentations, la première correspond à faire le produit de deux types, au sens mathématique du produit Cartésien. C'est aussi une construction qui existe dans les langages impératifs : construire une donnée qui en contient plusieurs autres, en quantité fixée. Le nom habituel pour ces constructions est enregistement (en anglais record), mais nous verrons rapidement que nous avons de bonnes raisons de les appeler types produits.

Un exemple de type produit est donné par le type des nombres complexes, dans le module Complex de la librairie standard. Un complexe est défini par deux valeurs réels, la partie réelle et la partie imaginaire. N'importe quelle paire de deux réels définit un nombre complexe, et inversement tout nombre complexe se décrit simplement à l'aide de deux réels. Il s'agit donc d'une bonne représentation (à ceci près que nous utilisons des flottants pour représenter les réels, ce qui n'est pas toujours une bonne représentation, mais cela suffit le plus souvent).

  1. type t = {
  2. re : float;
  3. im : float;
  4. }

Les types produits se construisent entre deux accolades, par une liste de champs. Chaque champ est déterminé par un identifiant, qui doit commencer par une minuscule, et à chaque champ est associé une information, une valeur. Les champs sont séparés par des point-virgules, l'ordre des champs n'a pas d'importance.

Lors de la définition d'un type produit, on doit indiquer pour chaque champ le type des informations associés au champ, en utilisant le caractère :. Lors de la construction d'un champ, à chaque champ est associé une valeur, en utilisant le caractère =.

Nous pouvons donc maintenant définir un nombre complexe :

  1. let a_complex =
  2. { re = 1.23; im = -0.2 }

et la syntaxe de construction est la même que la syntaxe d'observation :

  1. let rotate_90 { re = x; im = y } =
  2. { re = y; im = -. x }

Dans cette fonction de rotation, x est l'abstraction de la partie réelle et y l'abstraction de la partie imaginaire de l'argument.

Ce n'est pas toujours pratique d'utiliser les observations par motifs lorsqu'on manipule des enregistrements. Du coup il existe plein de possibilités syntaxiques pour rendre leur usage plus agréable. Prenons par exemple le type des dates du module Unix.

  1. type tm = {
  2. tm_sec : int; (* seconds 0..60 *)
  3. tm_min : int; (* Minutes 0..59 *)
  4. tm_hour : int; (* Hours 0..23 *)
  5. tm_mday : int; (* Day of month 1..31 *)
  6. tm_mon : int; (* Month of year 0..11 *)
  7. tm_year : int; (* Year - 1900 *)
  8. tm_wday : int; (* Day of week (Sunday is 0) *)
  9. tm_yday : int; (* Day of year 0..365 *)
  10. tm_isdst : bool; (* Daylight time savings in effect *)
  11. }

Clairement, nous n'avons pas envie d'observer tous les champs dès que nous cherchons à savoir l'heure qu'il est. On peut justement n'observer que les champs souhaités :

  1. let convert_and_pad_with_zeros int =
  2. let converted = string_of_int int in
  3. let prefix = String.init (2 - String.length convertex) (fun i -> '0') in
  4. prefix ^ converted

  5. let print_time { tm_hour = h; tm_min = m; tm_sec = s } =
  6. Printf.printf "%s:%s:%s\n%!"
  7. (convert_and_pad_with_zeros h)
  8. (convert_and_pad_with_zeros m)
  9. (convert_and_pad_with_zeros s)

Souvent, on n'observe qu'un enregistrement à la fois, dans ce cas plutôt qu'utiliser des noms supplémentaires pour les contenus des champs, on peut réutiliser son nom comme nom de variable :

  1. let print_time { tm_hour; tm_min; tm_sec } =
  2. Printf.printf "%s:%s:%s\n%!"
  3. (convert_and_pad_with_zeros tm_hour)
  4. (convert_and_pad_with_zeros tm_min)
  5. (convert_and_pad_with_zeros tm_sec)

Enfin, on peut simplement manipuler l'enregistrement comme une abstraction, et accéder au champ avec la "syntaxe pointée" :

  1. let print_time time =
  2. Printf.printf "%s:%s:%s\n%!"
  3. (convert_and_pad_with_zeros time.tm_hour)
  4. (convert_and_pad_with_zeros time.tm_min)
  5. (convert_and_pad_with_zeros time.tm_sec)

  6. (* No magic: time.tm_hour is sugar for (fun { tm_hour } -> tm_hour) time *)

Modifier un enregistrement.

Il n'est pas possible de modifier les champs d'un enregistrement.

En revanche, il existe une syntaxe pour créer facilement un nouvel enregistrement, qui est une copie d'un autre sauf pour quelques champs. Ainsi, pour arrondir une date à 0 seconde, on écrit :

  1. let round_sec time =
  2. { time with tm_sec = 0 }

L'argument time n'est pas modifié par cette fonction ! On ne peut jamais, jamais modifié le contenu d'un enregistrement, ceci n'est pas une exception. Simplement, une copie de time est créée, avec le champ tm_sec redéfini. On peut ainsi redéfinir autant de champ que souhaité :

  1. let previous_midnight time =
  2. { time with
  3. tm_sec = 0;
  4. tm_min = 0;
  5. tm_hour = 0
  6. }

Il s'agit uniquement d'un sucre syntaxique : le compilateur transforme ce code en une création d'une date complète avec les 9 champs, dont 6 sont recopiés depuis time.

Contenu des champs.

Chaque champ contient une valeur, d'un type déterminé par le champ. Ce type est quelconque : un champ peut avoir le type d'une fonction, ou un type paramétré, simplement : n'importe quel type, aucune restriction. En particulier, un champ peut contenir une fonction.

Retour au sommaire.