Section courante

A propos

Section administrative du site

Types de données : deftype, defrecord et reify

Motivation

Clojure est conçu selon un modèle d'abstractions. Il existe des abstractions pour les séquences, les collections, la capacité d'appel,... De plus, Clojure fournit de nombreuses implémentations de ces abstractions. Les abstractions sont spécifiées par des interfaces hôtes, et les implémentations par des classes hôtes. Bien que cela ait suffi pour initialiser le langage, Clojure manquait alors de mécanismes d'abstraction et d'implémentation de bas niveau similaires. Les protocoles et les types de données ajoutent des mécanismes puissants et flexibles pour l'abstraction et la définition des structures de données, sans compromis sur les fonctionnalités de la plateforme hôte.

Principes de base

Les fonctionnalités de type de données - `deftype`, `defrecord` et `reify` - fournissent le mécanisme permettant de définir des implémentations d'abstractions et, dans le cas de `reify`, des instances de ces implémentations. Les abstractions elles-mêmes sont définies par des protocoles ou des interfaces. Un type de données fournit un type hôte (nommé pour `deftype` et `defrecord`, anonyme pour `reify`), doté d'une structure (champs explicites pour `deftype` et `defrecord`, fermeture implicite pour `reify`) et des implémentations optionnelles de méthodes d'abstraction. Ils permettent d'accéder, de manière relativement simple, aux mécanismes de représentation primitive et de polymorphisme les plus performants de l'hôte. Il est important de noter qu'il ne s'agit pas de simples constructions encapsulant l'hôte. Ils ne prennent en charge qu'un sous-ensemble restreint des fonctionnalités de l'hôte, souvent avec un dynamisme supérieur à celui de l'hôte lui-même. L'objectif est que, sauf en cas d'interopérabilité nécessitant de dépasser leur portée, il ne soit pas nécessaire de quitter Clojure pour bénéficier des structures de données les plus performantes disponibles sur la plateforme.

deftype et defrecord

deftype et defrecord génèrent dynamiquement du bytecode compilé pour une classe nommée, avec un ensemble de champs spécifiés et, en option, des méthodes pour un ou plusieurs protocoles et/ou interfaces. Ils conviennent au développement dynamique et interactif, ne nécessitent pas de compilation AOT et peuvent être réévalués au cours d'une même session. Similaires à defstruct pour la génération de structures de données avec des champs nommés, ils diffèrent de defstruct par les points suivants :

Les classes `deftype` et `defrecord` diffèrent comme suit :

Pourquoi utiliser à la fois `deftype` et `defrecord` ?

Dans la plupart des programmes orientés objet, les classes se répartissent en deux catégories distinctes : celles qui sont des artefacts du domaine d'implémentation/de programmation, comme les classes `String` ou `Collections`, ou les types référence de Clojure ; et celles qui représentent des informations du domaine applicatif, comme `Employee`, `PurchaseOrder`, etc. L'utilisation de classes pour les informations du domaine applicatif a toujours eu pour inconvénient de masquer ces informations derrière des micro-langages spécifiques à chaque classe. Par exemple, même la méthode apparemment anodine `employee.getName()` est une interface personnalisée pour accéder aux données. Stocker des informations dans de telles classes pose problème, un peu comme si chaque livre était écrit dans une langue différente. On ne peut plus adopter une approche générique du traitement de l'information. Il en résulte une explosion de spécificités inutiles et une pénurie de réutilisation.

C'est pourquoi Clojure a toujours encouragé l'utilisation de maps pour ces informations, et ce conseil reste valable quel que soit le type de données. L'utilisation de `defrecord` permet d'obtenir des informations manipulables de manière générique, ainsi que les avantages du polymorphisme piloté par les types et l'efficacité structurelle des champs. En revanche, il est absurde qu'un type de données définissant une collection, comme `vector`, ait une implémentation par défaut de `map`. `deftype` est donc plus adapté à la définition de telles constructions de programmation.

De manière générale, les enregistrements sont supérieurs aux `structmap` pour toutes les opérations de gestion d'informations, et il est recommandé de migrer ces dernières vers `defrecord`. Il est peu probable que beaucoup de code utilise des `structmap` pour des constructions de programmation, mais si tel est le cas, `deftype` s'avérera bien plus approprié.

Les implémentations de `deftype`/`defrecord` compilées AOT peuvent convenir à certains cas d'utilisation de `gen-class`, lorsque leurs limitations ne sont pas rédhibitoires. Dans ces cas, leurs performances seront supérieures à celles de `gen-class`.

Les types de données et les protocoles sont subjectifs.

Bien que les types de données et les protocoles entretiennent des relations bien définies avec les constructions hôtes et constituent un excellent moyen d'exposer les fonctionnalités Clojure aux programmes Java, ils ne sont pas principalement conçus pour l'interopérabilité. Autrement dit, ils ne cherchent pas à imiter ou à s'adapter complètement à tous les mécanismes orientés objet de l'hôte. Ils reflètent notamment les opinions suivantes :

L'utilisation de types de données et de protocoles vous permettra de proposer une API claire et basée sur des interfaces à vos clients Java. Avec une API Java claire et basée sur des interfaces, les types de données et les protocoles peuvent être utilisés pour l'interopérabilité et l'extension. En revanche, avec une API Java de mauvaise qualité, vous devrez recourir à gen-class. Seule cette approche garantit que les constructions de programmation utilisées pour concevoir et implémenter vos programmes Clojure seront exemptes des complexités inhérentes à la programmation orientée objet.

reify

Alors que `deftype` et `defrecord` définissent des types nommés, `reify` définit un type anonyme et crée une instance de ce type. Son utilité réside dans le besoin d'une implémentation unique d'un ou plusieurs protocoles ou interfaces, en tirant parti du contexte local. À cet égard, son utilisation est similaire à celle des classes internes anonymes (`proxy`) en Java.

Les corps des méthodes de reify sont des fermetures lexicales et peuvent faire référence à la portée locale environnante. reify diffère de proxy par les points suivants :

Il en résulte de meilleures performances que le proxy, tant au niveau de la construction que de l'invocation. La réification est préférable au proxy dans tous les cas où ses contraintes ne sont pas prohibitives.

Prise en charge des annotations Java

Les types créés avec `deftype`, `defrecord` et `definterface` peuvent générer des classes incluant des annotations Java pour l'interopérabilité Java. Les annotations sont décrites comme des métadonnées sur :

Exemple :

  1. (import [java.lang.annotation Retention RetentionPolicy Target ElementType]
  2.         [javax.xml.ws WebServiceRef WebServiceRefs])
  3.  
  4. (definterface Foo (foo []))
  5.  
  6. ;; annotation sur le type
  7. (deftype ^{Deprecated true
  8.            Retention RetentionPolicy/RUNTIME
  9.            javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
  10.            javax.xml.ws.soap.Addressing {:enabled false :required true}
  11.            WebServiceRefs [(WebServiceRef {:name "fred" :type String})
  12.                            (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
  13.   Bar [^int a
  14.        ;; dans le champ
  15.        ^{:tag int
  16.          Deprecated true
  17.          Retention RetentionPolicy/RUNTIME
  18.          javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
  19.          javax.xml.ws.soap.Addressing {:enabled false :required true}
  20.          WebServiceRefs [(WebServiceRef {:name "fred" :type String})
  21.                          (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
  22.        b]
  23.   ;; dans la méthode
  24.   Foo (^{Deprecated true
  25.          Retention RetentionPolicy/RUNTIME
  26.          javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
  27.          javax.xml.ws.soap.Addressing {:enabled false :required true}
  28.          WebServiceRefs [(WebServiceRef {:name "fred" :type String})
  29.                          (WebServiceRef {:name "ethel" :mappedName "lucy"})]}
  30.        foo [this] 42))
  31.  
  32. (seq (.getAnnotations Bar))
  33. (seq (.getAnnotations (.getField Bar "b")))
  34. (seq (.getAnnotations (.getMethod Bar "foo" nil)))


Dernière mise à jour : Lundi, le 2 février 2026