| Fiche technique | |
|---|---|
| Type de produit : | Langage de programmation |
| Auteur : | Rich Hickey |
| Licence : | Licence Eclipse |
| Date de publication : | 2007 |
| Site Web : | https://clojure.org/ |
Introduction
Clojure est un langage de programmation dit homoiconique, un terme technique assez précis qui signifie que les programmes écrits en Clojure sont eux-mêmes représentés sous forme de structures de données Clojure. Cette caractéristique est fondamentale et distingue Clojure (tout comme son proche parent, Common Lisp) de la grande majorité des autres langages de programmation. En effet, contrairement à de nombreux langages traditionnels qui définissent la syntaxe en termes de flux de caractères ou de fichiers texte, Clojure est défini principalement en termes d'évaluation directe de structures de données. Cette approche confère au langage une grande puissance : il devient extrêmement naturel et simple pour un programme Clojure de manipuler, transformer, combiner ou même générer d'autres programmes Clojure, créant ainsi une flexibilité qui n'existe pas dans les langages plus conventionnels où le code et les données sont strictement séparés.
Cependant, malgré cette orientation vers les structures de données, la plupart des programmes Clojure sont initialement créés et entreposés sous forme de fichiers texte classiques, ce qui implique qu'ils doivent être interprétés par un composant spécial appelé lecteur (ou reader). Le rôle du lecteur est crucial : il analyse le texte brut, en extrait les différentes unités syntaxiques, et produit la structure de données Clojure correspondante que le compilateur ou l'évaluateur pourra ensuite lire et exécuter. Il est important de noter que cette étape ne se limite pas à une simple phase intermédiaire de compilation; le lecteur a une valeur en soi. Les structures qu'il produit peuvent être utilisées dans de nombreux contextes, de manière similaire à la façon dont XML ou JSON sont manipulés dans d'autres environnements. Ainsi, le lecteur n'est pas seulement un outil de prétraitement : il constitue un composant fondamental permettant d'exploiter pleinement la nature homoiconique de Clojure.
On peut schématiquement dire que le lecteur possède sa propre syntaxe, définie à un niveau de caractères et de flux textuels, tandis que le langage Clojure en lui-même définit sa syntaxe en termes de symboles, listes, vecteurs, dictionnaires et autres structures de données natives. Le lecteur est implémenté via la fonction read, qui prend comme entrée un flux de caractères et renvoie la forme Clojure complète correspondante, c'est-à-dire un objet représentant le code ou les données structurées, plutôt qu'un simple caractère isolé. Cette abstraction permet aux développeurs de traiter le code comme des données manipulables, ouvrant la voie à des transformations dynamiques, à la méta-programmation et à d'autres techniques avancées rarement aussi naturelles dans d'autres langages.
Puisque toute démarche pédagogique ou documentation doit débuter quelque part, cette référence commence précisément là où commence l'évaluation dans Clojure : avec les formes du lecteur. Aborder ces formes implique inévitablement de parler des structures de données fondamentales, de leurs propriétés et de la façon dont elles sont interprétées par le compilateur ou l'évaluateur. En explorant d'abord le lecteur et les structures qu'il produit, on jette les bases pour comprendre comment le langage Clojure fonctionne réellement, et comment il permet de combiner de manière fluide code et données, ouvrant ainsi la voie à des manipulations avancées, des métaprogrammes et à des approches très flexibles dans l'écriture de logiciels.
Formulaires de lecture
Symboles
- Les symboles commencent par un caractère non numérique et peuvent contenir des caractères alphanumériques ainsi que les symboles *, +, !, -, _, ', ?, <, > et = (d'autres caractères pourront être autorisés ultérieurement).
- Le caractère '/' a une signification particulière : il peut être utilisé une seule fois au milieu d'un symbole pour séparer l'espace de noms du nom, par exemple : my-namespace/foo. '/' seul désigne la fonction de division.
- Le caractère '.' a une signification particulière : il peut être utilisé une ou plusieurs fois au milieu d'un symbole pour désigner un nom de classe pleinement qualifié, par exemple : java.util.BitSet, ou dans les noms d'espaces de noms. Les symboles commençant ou se terminant par '.' sont réservés par Clojure. Les symboles contenant '/' ou '.' sont dits «qualifiés».
- Les symboles commençant ou se terminant par ':' sont réservés par Clojure. Un symbole peut contenir un ou plusieurs ':' non répétitifs.
Valeurs littérales
- Chaînes de caractères : placées entre guillemets doubles. Peuvent s'étendre sur plusieurs lignes. Les caractères d'échappement Java standard sont pris en charge.
- Nombres : généralement représentés conformément aux conventions Java.
- Les entiers peuvent être d'une longueur indéfinie et seront lus comme des `Long` lorsqu'ils sont compris dans la plage autorisée, et comme des `clojure.lang.BigInt` dans le cas contraire. Les entiers se terminant par `N` sont toujours lus comme des `BigInt`. La notation octale est autorisée avec le préfixe `0`, et la notation hexadécimale avec le préfixe `0x`. Lorsque cela est possible, ils peuvent être spécifiés dans n'importe quelle base, de 2 à 36 (voir `Long.parseLong()`). Par exemple, `2r101010`, `052`, `8r52`, `0x2a`, `36r16` et `42` sont tous de type `Long`.
- Les nombres à virgule flottante sont lus comme des `Double`. Avec le suffixe `M`, ils sont lus comme des `BigDecimal`.
- Les ratios sont pris en charge, par exemple `22/7`.
- Caractères : précédés d'une barre oblique inverse : \c. \newline, \space, \tab, \formfeed, \backspace et \return produisent les caractères correspondants. Les caractères Unicode sont représentés par \uNNNN comme en Java. Les caractères octaux sont représentés par \oNNN.
- nil : Signifie «rien/aucune valeur» - représente la valeur null en Java et teste la valeur logique fausse.
- Booléens : vrai et faux.
- Valeurs symboliques : ##Inf, ##-Inf et ##NaN.
- Mots clefs : Les mots clefs sont similaires aux symboles, à quelques exceptions près :
- Ils peuvent et doivent commencer par deux points, par exemple : `:fred`.
- Comme les symboles, ils peuvent contenir un espace de noms, `:person/name`, qui peut contenir des points.
- Un mot-clef commençant par deux points est automatiquement résolu dans l'espace de noms courant en un mot-clef qualifié :
- Si le mot-clef n'est pas qualifié, l'espace de noms est l'espace de noms courant. Dans l'espace de noms utilisateur, `::rect` est interprété comme `:user/rect`.
- Si le mot-clef est qualifié, l'espace de noms est résolu à l'aide des alias de l'espace de noms courant. Dans un espace de noms où `x` est un alias de `example`, `::x/foo` est résolu en `:example/foo`.
Listes
Les listes sont composées de zéro ou plusieurs éléments entre parenthèses : (a b c)
Vecteurs
Les vecteurs sont des nombres (zéro ou plusieurs) encadrés par des crochets : [1 2 3]
Cartes
- Une carte est composée de zéro ou plusieurs paires clef/valeur entre accolades : {:a 1 :b 2}
- Les virgules sont considérées comme des espaces et peuvent servir à organiser les paires : {:a 1, :b 2}
- Les clefs et les valeurs peuvent prendre n'importe quelle forme.
Syntaxe des espaces de noms Map
Ajoutée dans Clojure 1.9
Les littéraux de type map peuvent spécifier un contexte d'espace de noms par défaut pour les clefs de la map à l'aide du préfixe `#:ns`, où `ns` est le nom de l'espace de noms et le préfixe précède l'accolade ouvrante `{` de la map. De plus, `#::` peut être utilisé pour la résolution automatique des espaces de noms, de la même manière que les mots-clefs à résolution automatique.
Un littéral de type map avec syntaxe d'espace de noms se distingue d'un littéral de type map sans espace de noms par les différences suivantes :
- Keys
- Keys étant des mots-clefs ou des symboles sans espace de noms sont lues avec l'espace de noms par défaut.
- Keys étant des mots-clefs ou des symboles avec un espace de noms ne sont pas affectées, à l'exception de l'espace de noms spécial `_`, étant supprimé lors de la lecture. Ceci permet de spécifier des mots-clefs ou des symboles sans espace de noms comme clés dans une chaîne de caractères avec la syntaxe des espaces de noms.
- Keys qui ne sont ni des symboles ni des mots-clefs ne sont pas affectées.
- Values
- Les valeurs ne sont pas affectées.
- Les clefs littérales de la carte imbriquée ne sont pas affectées.
Par exemple, la représentation littérale de carte suivante avec la syntaxe d'espace de noms :
- #:person{:first "Han"
- :last "Solo"
- :ship #:ship{:name "Millennium Falcon"
- :model "YT-1300f light freighter"}}
se lit comme :
- {:person/first "Han"
- :person/last "Solo"
- :person/ship {:ship/name "Millennium Falcon"
- :ship/model "YT-1300f light freighter"}}
Ensembles
Un ensemble est une ou plusieurs formes encadrées par des accolades et précédées de # : #{:a :b :c}
deftype, defrecord, appels de constructeur
Ajouté dans Clojure 1.3
- Les appels aux constructeurs des classes Java `class`, `deftype` et `defrecord` peuvent être effectués en utilisant leur nom de classe pleinement qualifié, précédé du symbole `#` et suivi d'un vecteur : `#my.klass_or_type_or_record[:a :b :c]`.
- Les éléments du vecteur sont passés sans évaluation au constructeur correspondant. Les instances de `defrecord` peuvent également être créées de manière similaire en prenant une `map` : `#my.record{:a 1, :b 2}`.
- Les valeurs indexées de la `map` sont affectées sans évaluation aux champs correspondants de l'instance `defrecord`. Les champs de `defrecord` sans entrée correspondante dans la `map` littérale se voient attribuer la valeur `nil`. Toute valeur indexée supplémentaire dans la `map` littérale est ajoutée à l'instance `defrecord` résultante.
Caractères macro
Le comportement du lecteur est déterminé par une combinaison de structures intégrées et d'un système d'extension appelé table de lecture. Les entrées de la table de lecture établissent des correspondances entre certains caractères, appelés caractères macro, et des comportements de lecture spécifiques, appelés macros de lecture. Sauf indication contraire, les caractères macro ne peuvent pas être utilisés dans les symboles utilisateur.
Citation (')
- 'form ⇒ (quote form)
Caractère (\)
Comme indiqué ci-dessus, cette combinaison renvoie un caractère littéral. Exemples de caractères littéraux : \a, \b, \c.
Les caractères spéciaux suivants peuvent être utilisés pour les caractères courants : \newline, \space, \tab, \formfeed, \backspace et \return.
La prise en charge d'Unicode suit les conventions Java et dépend de la version de Java sous-jacente. Un caractère littéral Unicode est de la forme \uNNNN ; par exemple, \u03A9 représente Ω.
Commentaire (;)
Un commentaire sur une seule ligne invite le lecteur à ignorer tout ce qui se trouve entre le point-virgule et la fin de la ligne.
Deref (@)
- @form ⇒ (deref form)
Métadonnées (^)
Les métadonnées sont un ensemble de correspondances entre certains types d'objets : symboles, listes, vecteurs, ensembles, dictionnaires, littéraux étiquetés renvoyant un objet IMeta, enregistrements, types et appels de constructeurs. La macro de lecture des métadonnées commence par lire les métadonnées et les associe à l'élément suivant lu (voir `with-meta` pour associer des métadonnées à un objet) :
- ^{:a 1 :b 2} [1 2 3]
donne le vecteur :
- [1 2 3]
avec une carte de métadonnées de :
- {:a 1 :b 2}
Une version abrégée permet aux métadonnées d'être un simple symbole ou une chaîne de caractères, auquel cas elles sont traitées comme une carte à entrée unique avec une clef de :tag et une valeur du symbole ou de la chaîne (résolue), par exemple :
- ^String x
est la même chose que :
- ^{:tag java.lang.String} x
Une version abrégée des signatures de type permet aux métadonnées d'être un vecteur. Dans ce cas, elles sont traitées comme une seule entrée d'une carte dont la clef est `:param-tags` et la valeur correspond aux indications de type (résolues), à un vecteur de valeurs `:tag` ou à `_`. Par exemple : `^[String long _]` est équivalent à `^{:param-tags [java.lang.String long _]}`. Consultez la documentation sur `:param-tags` pour savoir comment utiliser les balises de param.
Une autre version abrégée permet aux métadonnées d'être un mot-clé. Dans ce cas, elles sont traitées comme une seule entrée d'une carte dont la clef est le mot-clef et la valeur `true` :
- ^:dynamic x
est la même chose que :
- ^{:dynamic true} x
Les métadonnées peuvent être chaînées, auquel cas elles sont fusionnées de droite à gauche.
dispatch (#)
La macro dispatch fait en sorte que le lecteur utilise une macro de lecture provenant d'une autre table, indexée par le caractère suivant :
- #{} - voir les ensembles ci-dessus
- Regex patterns (#"pattern")
- Un motif d'expression régulière est lu et compilé lors de la lecture. L'objet résultant est de type `java.util.regex.Pattern`. Les expressions régulières ne suivent pas les mêmes règles d'échappement que les chaînes de caractères. Plus précisément, les barres obliques inverses dans le motif sont traitées comme telles (et n'ont pas besoin d'être échappées par une barre oblique inverse supplémentaire). Par exemple, `(re-pattern "\\s*\\d+")` peut s'écrire plus concisément `#"\s*\d+"`.
- Var-quote (#') :
- #'x ⇒ (var x)
- Fonction littérale anonyme (#()) :
- #(...) ⇒ (fn [args] (...))
- Ignorer le formulaire suivant (#_) : Le formulaire suivant #_ est complètement ignoré par le lecteur. (Cette suppression est plus complète que celle de la macro de commentaire, qui renvoie nil).
Les args sont déterminés par la présence de littéraux de la forme %, %n ou %&. % est synonyme de %1, %n désigne le n-ième argument (à partir de 1) et %& désigne un argument restant. Ceci ne remplace pas fn ; son utilisation idiomatique se limite à des fonctions de mappage/filtrage ponctuelles et très courtes. Les formes #() ne peuvent pas être imbriquées.
Guillemets syntaxiques (`, notez le caractère « guillemet inversé »), suppression des guillemets (~) et suppression des guillemets (~@)
Pour toutes les formes autres que les symboles, les listes, les vecteurs, les ensembles et les dictionnaires, `x` est équivalent à 'x.
Pour les symboles, les guillemets syntaxiques résolvent le symbole dans le contexte courant, produisant un symbole pleinement qualifié (c'est-à-dire espace de noms/nom ou pleinement.qualifié.nom.de.classe). Si un symbole n'est pas qualifié par un espace de noms et se termine par '#', il est résolu en un symbole généré portant le même nom auquel on a ajouté '_' et un identifiant unique. Par exemple, x# sera résolu en x_123. Toutes les références à ce symbole dans une expression entre guillemets syntaxiques sont résolues en ce même symbole généré.
Pour les listes, les vecteurs, les ensembles et les dictionnaires, les guillemets syntaxiques établissent un modèle de la structure de données correspondante. Dans le modèle, les formes non qualifiées se comportent comme si elles étaient encadrées par des guillemets syntaxiques récursifs. Cependant, il est possible d'exclure les formes de cet encadrement récursif en les qualifiant avec `unquote` ou `unquote-splicing`. Dans ce cas, elles seront traitées comme des expressions et remplacées dans le modèle par leur valeur, ou leur séquence de valeurs, respectivement.
Par exemple :
- user=> (def x 5)
- user=> (def lst '(a b c))
- user=> `(fred x ~x lst ~@lst 7 8 :nine)
- (user/fred user/x 5 user/lst a b c 7 8 :nine)
La table de lecture n'est actuellement pas accessible aux programmes utilisateurs.
Notation de données extensible (edn)
Le lecteur Clojure prend en charge un sur-ensemble de la notation de données extensible (edn). La spécification edn est en cours de développement et complète ce document en définissant un sous-ensemble de la syntaxe de données Clojure de manière indépendante du langage.
Littéraux étiquetés
Les littéraux étiquetés sont l'implémentation Clojure des éléments étiquetés edn.
Au démarrage, Clojure recherche les fichiers nommés data_readers.clj ou data_readers.cljc à la racine du classpath. Chaque fichier de ce type doit contenir une table de hachage Clojure des symboles, comme ceci :
- {foo/bar my.project.foo/bar
- foo/baz my.project/baz}
Dans chaque paire, la clef est une étiquette étant reconnue par le lecteur Clojure. La valeur est le nom complet d'une variable (Var) que le lecteur appellera pour analyser le formulaire suivant l'étiquette. Par exemple, avec le fichier data_readers.clj ci-dessus, le lecteur Clojure analyserait ce formulaire :
- #foo/bar [1 2 3]
En invoquant la variable `#'my.project.foo/bar'` sur le vecteur `[1 2 3]`, la fonction de lecture de données est appelée après que le vecteur ait été lu comme une structure de données Clojure classique. Pour vos propres fonctions de lecture de données, vous devez signaler les erreurs en levant des exceptions `RuntimeException` accompagnées de messages d'information.
Les balises de lecture sans qualificateur d'espace de noms sont réservées à Clojure. Les balises de lecture par défaut sont définies dans `default-data-readers`, mais peuvent être redéfinies dans `data_readers.clj` / `data_readers.cljc` ou en redéfinissant `*data-readers*`. Si aucun lecteur de données n'est trouvé pour une balise, la fonction associée à `default-data-reader-fn` sera appelée avec la balise et la valeur pour produire une valeur. Si `default-data-reader-fn` est `nil` (valeur par défaut), une exception `RuntimeException` sera levée.
Si un fichier data_readers.cljc est fourni, il est lu avec la même sémantique que n'importe quel autre fichier source cljc avec des conditions de lecture.
Littéraux étiquetés intégrés
Clojure 1.4 a introduit les littéraux étiquetés instant et UUID. Les instants ont le format suivant : `#inst "yyyy-mm-ddThh:mm:ss.fff+hh:mm"`. Remarque : certains éléments de ce format sont optionnels. Consultez le code pour plus de détails. Par défaut, le lecteur analysera la chaîne fournie et la convertira en un objet `java.util.Date`. Par exemple :
- (def instant #inst "2018-03-28T10:48:00.000")
- (= java.util.Date (class instant))
- ;=> true
Puisque *data-readers* est une variable dynamique pouvant être liée, vous pouvez remplacer le lecteur par défaut par un autre. Par exemple, clojure.instant/read-instant-calendar analysera la valeur littérale en un objet java.util.Calendar, tandis que clojure.instant/read-instant-timestamp l'analysera en un objet java.util.Timestamp :
- (binding [*data-readers* {'inst read-instant-calendar}]
- (= java.util.Calendar (class (read-string (pr-str instant)))))
- ;=> true
-
- (binding [*data-readers* {'inst read-instant-timestamp}]
- (= java.util.Timestamp (class (read-string (pr-str instant)))))
- ;=> true
La valeur littérale étiquetée #uuid sera analysée et convertie en un objet java.util.UUID :
- (= java.util.UUID (class (read-string "#uuid \"3b8a31ed-fd89-4f1b-a00f-42e3d60cf5ce\"")))
- ;=> true
Fonction de lecture de données par défaut
Si aucun lecteur de données n'est trouvé lors de la lecture d'une valeur littérale étiquetée, la fonction `default-data-reader-fn` est appelée. Vous pouvez définir votre propre fonction de lecture de données par défaut. La fonction `tagged-literal` fournie permet de créer un objet capable d'entreposer une valeur littérale non traitée. L'objet renvoyé par `tagged-literal` prend en charge la recherche par mot-clef des balises `:tag` et `:form:`.
- (set! *default-data-reader-fn* tagged-literal)
-
- ;; lire #objet comme un objet TaggedLiteral générique
- (def x #object[clojure.lang.Namespace 0x23bff419 "user"])
-
- [(:tag x) (:form x)]
- ;=> [object [clojure.lang.Namespace 599782425 "user"]]
Conditionnelles de lecture
Clojure 1.7 a introduit une nouvelle extension (.cljc) pour les fichiers portables, compatibles avec plusieurs plateformes Clojure. Le principal mécanisme de gestion du code spécifique à une plateforme consiste à isoler ce code dans un ensemble minimal d'espaces de noms, puis à fournir des versions spécifiques à chaque plateforme (.clj/.class ou .cljs) de ces espaces de noms.
Lorsqu'il est impossible d'isoler les différentes parties du code, ou lorsque le code est majoritairement portable avec seulement quelques petites parties spécifiques à une plateforme, la version 1.7 a également introduit les conditionnelles de lecture, prises en charge uniquement dans les fichiers .cljc et dans l'interpréteur interactif (REPL) par défaut. Il est recommandé d'utiliser les conditionnelles de lecture avec parcimonie et uniquement lorsque cela est nécessaire.
Les conditionnelles de lecture sont une nouvelle forme de dispatch du lecteur commençant par #? ou #?@. Elles consistent en une série de fonctionnalités et d'expressions alternées, similaires à cond. Chaque plateforme Clojure possède une « fonctionnalité de plateforme » bien connue : :clj, :cljs, :cljr. Chaque condition d'une instruction conditionnelle de lecture est vérifiée séquentiellement jusqu'à ce qu'une fonctionnalité correspondant à celle de la plateforme soit trouvée. L'instruction conditionnelle de lecture lira et renverra l'expression de cette fonctionnalité. L'expression de chaque branche non sélectionnée sera lue, mais ignorée. Une fonctionnalité `:default` bien connue correspondra toujours et peut être utilisée pour définir une valeur par défaut. Si aucune branche ne correspond, aucun formulaire ne sera lu (comme si aucune instruction conditionnelle de lecture n'était présente).
Les développeurs de plateformes Clojure non officielles doivent utiliser un mot-clef qualifié pour leur fonctionnalité de plateforme afin d'éviter les conflits de noms. Les fonctionnalités de plateforme non qualifiées sont réservées aux plateformes officielles.
L'exemple suivant sera interprété comme `Double/NaN` en Clojure, `js/NaN` en ClojureScript et `nil` sur toute autre plateforme :
- #?(:clj Double/NaN
- :cljs js/NaN
- :default nil)
La syntaxe de #?@ est identique, mais l'expression doit renvoyer une collection pouvant être insérée dans le contexte environnant, à l'instar de l'insertion conditionnelle entre guillemets dans la syntaxe quote. L'utilisation de l'insertion conditionnelle du lecteur au niveau supérieur n'est pas prise en charge et lèvera une exception. Exemple :
- [1 2 #?@(:clj [3 4] :cljs [5 6])]
- ;; dans clj => [1 2 3 4]
- ;; dans cljs => [1 2 5 6]
- ;; ailleurs autrement => [1 2]
Les fonctions `read` et `read-string` acceptent éventuellement une carte d'options comme premier argument. Les fonctionnalités actuelles et le comportement conditionnel du lecteur peuvent être définis dans cette carte d'options à l'aide des clefs et valeurs suivantes :
- :read-cond - :allow to process reader conditionals, or
- :preserve to keep all branches
- :features - persistent set of feature keywords that are active
Exemple de test des conditions de lecture ClojureScript depuis Clojure :
- (read-string
- {:read-cond :allow
- :features #{:cljs}}
- "#?(:cljs :works! :default :boo)")
- ;; :works!
Notez toutefois que le lecteur Clojure injectera toujours la fonctionnalité de plateforme `:clj`. Pour une lecture indépendante de la plateforme, consultez `tools.reader`.
Si le lecteur est invoqué avec `{:read-cond :preserve}`, la condition de lecture et les branches non exécutées seront conservées, en tant que données, dans le résultat retourné. La condition de lecture sera retournée sous la forme d'un type prenant en charge la récupération par mot-clé pour les clés avec `:form` et l'indicateur `:splicing?`. Les littéraux étiquetés lus mais ignorés seront retournés sous la forme d'un type prenant en charge la récupération par mot-clé pour les clefs avec `:form` et `:tag`.
- (read-string
- {:read-cond :preserve}
- "[1 2 #?@(:clj [3 4] :cljs [5 6])]")
- ;; [1 2 #?@(:clj [3 4] :cljs [5 6])]
Les fonctions suivantes peuvent également être utilisées comme prédicats ou constructeurs pour ces types :
- reader-conditional? reader-conditional tagged-literal? tagged-literal