Cours 4

Htmligure
Version & licenses
Creative Commons License

Types sommes

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Les types sommes.

Nous venons de voir une façon de construire de nouvelles représentations, et donc de nouveau types, les enregistrements. Un enregistrement permet de représenter plusieurs valeurs simultanément. Tous les champs d'un enregistrement doivent être définis, le choix d'une valeur pour chacun des champs définit dont un enregistrement.

On peut vouloir faire la construction duale, c'est-à-dire avoir une représentation dans laquelle seulement un "champ" est défini, plutôt que de demander tous les champs. C'est le but des types sommes.

Un type somme code donc une alternative entre deux (ou plus) constructions possibles. Une valeur d'un type somme pourra donc en général être construite, et donc observer, de plusieurs façons. Il faut donc plusieurs constructeurs différents pour le type, et pour chaque constructeur le type de l'information associé.

Prenons l'exemple d'un vendeur par correspondance de livres et de disques. Il vend des objets qui sont soit des livres, soit des disques.

  1. type book = {
  2. title : string;
  3. author : string;
  4. isbn : int
  5. }

  6. type kind =
  7. | CD
  8. | DVD
  9. | Vinyl

  10. type disc = {
  11. name : string;
  12. composer : string;
  13. kind : kind;
  14. uid : int
  15. }

book et disc sont des types produits, chaque livre doit avoir un titre et un auteur et un numéro d'identification international ISBN. De même chaque disque doit avoir un nom, un composeur et un identifiant unique, et est d'un type. Par contre il est soit un CD, soit un DVD, soit un vinyle, exactement l'un des trois. On introduit donc le type kind, qui est un type somme possédant trois constructeurs possibles, correspondant aux trois sortes de disques.

Une règle syntaxique est que les constructeurs doivent commencer par une majuscule. Comme dans le filtrage par motif, les différents cas sont séparés par des barres verticales, qui ont souvent en informatique le rôle d'indiquer une alternative entre plusieurs choix (comme dans une disjonction logique). On peut maintenant construire des valeurs qui sont des livres ou des disques :

  1. let a_disc =
  2. { name = "Out of the Blue";
  3. composer = "System F";
  4. kind = CD;
  5. uid = 123456789;
  6. }

CD est un constructeur sans argument, c'est donc aussi une valeur (comme le sont [] et None. On peut donc l'utiliser directement pour définir le champ kind.

Nous avons donc des représentations adaptées des objets à vendre. Nous voulons maintenant un système pour gérer le traitement des colis envoyés aux clients. On peut abstraire les différents types d'objets vendus par un seul type :

  1. type item =
  2. | Book of book
  3. | Disc of disc

À nouveau un type somme : un objet est soit un livre, soit un disque. Cette fois, on associe à chaque constructeur une information adaptée, ce qui se fait avec le mot-clé of suivi du type de l'information associé à chaque constructeur.

Un colis est d'abord une commande d'une liste d'items, à envoyer à une adresse. Une fois préparé il est envoyé à une adresse, et un numéro de colis lui est attribué. Enfin, à la réception, il change de statut et devient délivré. Le cycle de vie d'un colis contient donc trois étapes, et on peut les représenter ainsi :

  1. type preparation_info =
  2. {
  3. items : (item * int) list;
  4. delivery_address : address;
  5. }

  6. type tracking_number = int

  7. type package_status
  8. | InPreparation of preparation_info
  9. | Sent of (address * tracking_number)
  10. | Delivered

Le filtrage par motifs est particulièrement adapté à observer les types sommes, puisqu'il permet d'essayer différents motifs pour trouver comment la valeur a été construite. Tous les types sommes se manipulent donc en premier lieu par filtrage par motifs, au moins jusqu'à avoir défini des fonctions de haut-niveau permettant d'abstraire la représentation.

Écrivons une fonction pour le traitement des colis :

  1. let prepare_package { items; delivery_address } =
  2. let package = List.fold_left Package.add Package.empty items in
  3. let tracking_number = DeliveryService.drop package delivery_address in
  4. Sent (delivery_address, tracking_number)

  5. let update_package_status = function
  6. | InPreparation info -> prepare_package info
  7. | Sent (address, tracking_number)
  8. when DeliveryService.is_delivered tracking_number -> Delivered
  9. | Sent info as status -> status
  10. | Delivered -> Delivered

Ici nous utilisons un cas par constructeur possible, et même un cas supplémentaire pour les colis en cours d'acheminement, en utilisant une garde (when) pour tester si le colis est arrivé. On peut donner un nom à l'information associée à un constructeur qui peut être un identifiant arbitraire (commençant par une minuscule puisque c'est une variable). On peut aussi observer l'information, comme toujours avec un niveau de profondeur arbitraire. Ainsi, le premier filtre pour Sent utilise un motif de paire, le second utiliser simplement une abstraction qui contiendra la paire.

Ce petit exemple simpliste permet d'illustrer comment les types sommes et produits permettent de représenter assez fidèlement les informations que nous souhaitons manipuler. Les produits représentent une conjonction de plusieurs informations, les sommes représentent une information parmi plusieurs possibles. Ces deux formes de constructions de types permettent une richesse suffisante pour la majorité des applications. Les types sommes et produits forment ensemble ce qu'on appelle les types algébriques (en anglais Algebraic Data Types ADT, à ne pas confondre avec les Abstract Data Types, aussi ADT, qui sont les structures de données abstraites, donc des interfaces de structures de données). C'est algébrique au sens où il y a une addition et une multiplication. Notons aussi qu'en considérant les types comme des ensembles de valeurs, les types sommes sont des unions, et les types produits sont des produits Cartésiens.

Types paramétrés.

Les types produits et les types sommes peuvent être paramétrés. Si une des informations définies dans le type est polymorphe, c'est-à-dire qu'elle peut être d'un type arbitraire, le type algébrique doit être paramétré par le type polymorphe. Prenons quelques exemples.

Les options sont définies avec deux constructeurs, soit l'option contient une information, soit elle ne contient rien. Les options sont un type somme :

  1. type 'value option =
  2. | None
  3. | Some of 'value

Il n'y a pas de raison de restreindre le type de l'information associée au constructeur Some : on peut vouloir utiliser des options d'entiers, de chaînes, de listes,... On attribue donc une variable de type au constructeur Some. Puisqu'un des constructeurs contient la variable de type 'value, il est obligatoire que le type option lui-même soit paramétré par ce type. C'est ce qui permet de distinguer les options de valeurs entières des options de valeurs booléennes par exemple.Les paramètres de types sont précisés entre le mot-clé type et le nom du nouveau type.

Cela fonctionne aussi pour les types produits, et en présence de plusieurs types polymorphes. Par exemple, si on souhaitait redéfinir le type des paires, puisqu'une paire contient simultanément deux valeurs, on utiliserait un type produit :

  1. type ('fst,'snd) pair =
  2. { first : 'fst;
  3. second : 'snd
  4. }

Ce type a deux paramètres, un pour chaque champ.

La libraire standard, depuis la version 4.0 3 , propose un type pour les résultats de fonctions pouvant soit retourner une valeur, soit échouer avec une autre valeur, typiquement un message d'erreur ou une explication. Ce type était utilisé par de nombreuses librairies qui le redéfinissait à chaque fois, il est donc maintenant intégré à la base pour que toutes les librairies s'accordent sur sa définition.

  1. type ('ok, 'err) result =
  2. | Ok of 'ok
  3. | Error of 'err

Cela ressemble un peu à la paire, mais un résultat ne possède qu'une valeur d'un seul des deux types possibles. C'est un cas particulier d'un autre type :

  1. type ('left,'right) either =
  2. | Left of 'left
  3. | Right of 'right

Le type either permet de coder explicitement une alternative entre deux types.

La librairie standard contient de nombreux exemples de types produits. Le module Unix en contient en grand nombre, pour encoder les enum des librairies systèmesde C par exemple. Mais aussi pour les adresses de sockets par exemple :

  1. type sockaddr =
  2. | ADDR_UNIX of string
  3. | ADDR_INET of inet_addr * int (* address and port *)

Les types sommes sont donc très largement utilisés dès qu'il s'agit de construire des représentations bas-niveaux de données.

Retour au sommaire.