Formes spéciales
Les formes spéciales possèdent des règles d'évaluation différentes de celles de Clojure standard et sont directement interprétées par le compilateur Clojure.
L'en-tête de chaque forme spéciale décrit sa grammaire de manière informelle, à l'aide de la syntaxe des expressions régulières : ? (optionnel), * (zéro ou plus) et + (un ou plus). Les éléments non terminaux sont indiqués en italique.
(def symbol doc-string? init?)
Crée et entrepose une variable globale nommée `symbol` et dont l'espace de noms correspond à la valeur de l'espace de noms courant (`*ns*`). Si `init` est fourni, il est évalué et la liaison racine de la variable est affectée à la valeur résultante. Sinon, la liaison racine reste inchangée. `def` s'applique toujours à la liaison racine, même si la variable est liée au thread au moment de son appel. `def` renvoie la variable elle-même (et non sa valeur). Lève une exception si `symbol` existe déjà dans l'espace de noms et n'est pas associé à une variable stockée. La prise en charge des chaînes de documentation a été ajoutée dans Clojure 1.3.
Toute métadonnée associée à `symbol` sera évaluée et deviendra une métadonnée de la variable elle-même. Certaines clefs de métadonnées ont une interprétation particulière :
| Clef | Description |
|---|---|
| :private | Un booléen indiquant le niveau d'accès de la variable. Si cette clé est absente, l'accès par défaut est public (comme si `:private` était défini sur `false`). |
| :doc | Une chaîne de caractères contenant une brève documentation (1 à 3 lignes) du contenu de la variable. |
| :test | Une fonction sans argument utilisant `assert` pour vérifier différentes opérations. La variable elle-même sera accessible lors de l'évaluation d'une fonction littérale dans la table de métadonnées. |
| :tag | Un symbole nommant une classe ou un objet `Class` indiquant le type Java de l'objet dans la variable, ou sa valeur de retour si l'objet est une fonction. |
De plus, le compilateur ajoutera les clefs de métadonnées suivantes à la variable :
| Clef | Description |
|---|---|
| :file | Chaîne de caractères |
| :line | Entier |
| :name | Symbole simple |
| :ns | Espace de noms dans lequel la variable est internée |
| :macro | Vrai si la variable désigne une macro |
| :arglists | Liste de vecteurs contenant les formes des arguments, tels qu'ils ont été fournis à la fonction defn |
Les métadonnées var peuvent également servir à des fins spécifiques à l'application. Il est recommandé d'utiliser des clés qualifiées par espace de noms (par exemple : myns/foo) afin d'éviter les conflits :
- (defn
- ^{:doc "mymax [xs+] obtient la valeur maximale dans xs en utilisant > "
- :test (fn []
- (assert (= 42 (mymax 2 42 5 4))))
- :user/comment "C'est le meilleur fn de tous les temps !"}
- mymax
- ([x] x)
- ([x y] (if (> x y) x y))
- ([x y & more]
- (reduce mymax (mymax x y) more)))
-
- user=> (meta #'mymax)
- {:name mymax,
- :user/comment "C'est le meilleur fn de tous les temps !",
- :doc "mymax [xs+] obtient la valeur maximale dans xs en utilisant > ",
- :arglists ([x] [x y] [x y & more])
- :file "repl-1",
- :line 126,
- :ns #<Namespace user >,
- :test #<user$fn__289 user$fn__289@20f443 >}
De nombreuses macros se développent en `def` (par exemple, `defn`, `defmacro`), et transmettent ainsi des métadonnées à la variable résultante à partir du symbole utilisé comme nom.
Utiliser `def` pour modifier la valeur racine d'une variable à un niveau autre que le niveau supérieur indique généralement que vous utilisez la variable comme une variable globale mutable, ce qui est considéré comme une mauvaise pratique. Il est préférable d'utiliser une liaison pour fournir une valeur locale au processus léger pour la variable, ou de placer une référence ou un agent dans la variable et d'utiliser des transactions ou des actions pour la modification.
(if test then else?)
La fonction `if` évalue le test. Si la valeur n'est ni `nil` ni `false`, elle l'évalue et renvoie `then`. Sinon, elle l'évalue et renvoie `else`. Si `else` n'est pas fourni, sa valeur par défaut est `nil`. Toutes les autres conditions en Clojure reposent sur la même logique : `nil` et `false` représentent la fausseté logique, et toute autre valeur représente la vérité logique. Ces significations s'appliquent à l'ensemble du code. La fonction `if` effectue des tests conditionnels sur les valeurs de retour booléennes des méthodes Java sans conversion en `Boolean`. Notez que `if` ne teste pas les valeurs arbitraires de `java.lang.Boolean`, mais uniquement la valeur `false` (équivalente à `Boolean.FALSE` en Java). Par conséquent, si vous créez vos propres booléens encapsulés, veillez à utiliser `Boolean/valueOf` et non les constructeurs de `Boolean`.
(do expr*)
Évalue les expressions exprs dans l'ordre et renvoie la valeur de la dernière. Si aucune expression n'est fournie, renvoie nil.
(let [ binding* ] expr*)
| binding ↠ binding-form init-expr |
Évalue les expressions exprs dans un contexte lexical où les symboles des formes de liaison sont liés à leurs expressions initiales respectives ou à des parties de celles-ci. Les liaisons sont séquentielles, chaque liaison pouvant donc accéder aux liaisons précédentes. Les expressions exprs sont contenues dans une instruction do implicite. Si un symbole de liaison est annoté avec une balise de métadonnées, le compilateur tentera de résoudre cette balise en un nom de classe et présumera de ce type lors des références ultérieures à la liaison. La forme de liaison la plus simple est un symbole, lié à l'expression initiale entière.
- (let [x 1
- y x]
- y)
- -> 1
Si le symbole de liaison :tag metadata est une interface Java annotée comme FunctionalInterface, l'expression d'initialisation sera convertie (si nécessaire) à l'interface spécifiée :
- (let [coll (java.util.ArrayList. (range 10))
- ^java.util.function.Predicate pred even?]
- (.removeIf coll pred) ;; mutation coll
- coll)
- -> [1 3 5 7 9]
Consultez la section « Formulaires de liaison » pour plus d'informations.
Les variables locales créées avec `let` ne sont pas des variables. Une fois créées, leurs valeurs restent inchangées !
(quote form)
Donne la forme non évaluée :
- user=> '(a b c)
- (a b c)
Notez qu'aucune tentative n'est faite pour appeler la fonction a. La valeur de retour est une liste de 3 symboles.
(var symbol)
Le symbole doit correspondre à une variable, et c'est l'objet Var lui-même (et non sa valeur) qui est renvoyé. La macro de lecture #'x se développe en (var x).
(fn name? [params* ] expr*)
(fn name? ([params* ] expr*)+)
|
params ↠ positional-param* , ou positional-param* & rest-param positional-param ↠ binding-form rest-param ↠ binding-form name ↠ symbol |
Définit une fonction (fn). Les fonctions (fn) sont des objets de première classe implémentant l'interface IFn. Cette interface définit une fonction invoke() surchargée avec une arité comprise entre 0 et 20. Un objet fn peut implémenter une ou plusieurs méthodes invoke, et donc être surchargé sur l'arité. Une seule surcharge peut être variadique, en spécifiant l'esperluette (&) suivie d'un paramètre restant (rest-param). Lorsqu'un tel point d'entrée variadique est appelé avec des arguments dépassant les paramètres positionnels, il les collecte dans une séquence (seq) liée au paramètre restant (rest-param) ou déstructurée par celui-ci. Si les arguments fournis ne dépassent pas les paramètres positionnels, le paramètre restant est nul.
La première forme définit une fonction (fn) avec une seule méthode invoke. La seconde définit une fonction (fn) avec une ou plusieurs surcharges de la méthode invoke. Les arités des surcharges doivent être distinctes. Dans les deux cas, le résultat de l'expression est un objet fn unique.
Les expressions `exprs` sont compilées dans un environnement où les paramètres sont liés aux arguments effectifs. Elles sont incluses dans une instruction `do` implicite. Si un symbole de nom est fourni, il est lié à l'objet fonction lui-même dans la définition de la fonction, permettant ainsi l'auto-appel, même dans les fonctions anonymes. Si un symbole de paramètre est annoté avec une balise de métadonnées, le compilateur tentera de résoudre cette balise en un nom de classe et présumera de ce type lors des références ultérieures à la liaison.
- (def mult
- (fn this
- ([] 1)
- ([x] x)
- ([x y] (* x y))
- ([x y & more]
- (apply this (this x y) more))))
Notez que les fonctions nommées, telles que `mult`, sont généralement définies avec `defn`, qui se développe comme ci-dessus.
Une surcharge de fonction (`fn`) définit un point de récursion en début de fonction, dont l'arité est égale au nombre de paramètres, y compris le paramètre `rest` s'il est présent. Voir `recur`.
Les fonctions implémentent les interfaces Java `Callable`, `Runnable` et `Comparator`.
Depuis la version 1.1
Les fonctions permettent de spécifier des pré- et post-conditions d'exécution.
La syntaxe des définitions de fonctions est la suivante :
(fn name? [param* ] condition-map? expr*)
(fn name? ([param* ] condition-map? expr*)+)
L'extension syntaxique s'applique également à `defn` et aux autres macros qui se développent en `fn`.
Remarque : Si la seule forme suivant le vecteur de paramètres est une table de correspondance, elle est interprétée comme le corps de la fonction et non comme la table de correspondance des conditions.
Le paramètre de table de correspondance des conditions permet de spécifier les pré- et post-conditions d'une fonction. Il se présente sous la forme suivante :
|
{:pre [pre-expr*] :post [post-expr*]} |
où chaque clef est facultative. La table de conditions peut également être fournie en tant que métadonnées de la liste d'arguments.
Les expressions pré- et post-définies sont des expressions booléennes pouvant faire référence aux paramètres de la fonction. De plus, le symbole % peut être utilisé dans une expression post-définie pour faire référence à la valeur de retour de la fonction. Si l'une des conditions est fausse et que l'assertion est vraie, une exception java.lang.AssertionError est levée.
Exemple :
- (defn constrained-sqr [x]
- {:pre [(pos? x)]
- :post [(> % 16), (< % 225)]}
- (* x x))
Consultez la section «Formulaires de reliure» pour plus d'informations sur les formulaires de reliure.
(loop [binding* ] expr*)
La boucle `loop` est identique à `let`, à ceci près qu'elle établit un point de récursion au début de la boucle, dont l'arité est égale au nombre d'itérations. Voir `recur`.
(recur expr*)
Évalue les expressions `exprs` séquentiellement, puis, en parallèle, réaffecte les liaisons du point de récursion aux valeurs de `exprs`. Si le point de récursion était une méthode `fn`, les paramètres sont réaffectés. Si le point de récursion était une boucle, les liaisons de la boucle sont réaffectées. L'exécution revient ensuite au point de récursion. L'expression `recur` doit correspondre exactement à l'arité du point de récursion. En particulier, si le point de récursion était le sommet d'une méthode `fn` variadique, aucun regroupement des arguments restants n'est effectué ; un seul `seq` (ou `null`) doit être passé. L'utilisation de `recur` ailleurs qu'en position terminale est une erreur.
Notez que `recur` est la seule construction de boucle n'utilisant pas la pile en Clojure. Il n'y a pas d'optimisation pour les appels terminaux et l'utilisation d'auto-appels pour boucler sur des bornes inconnues est déconseillée. `recur` est fonctionnel et son utilisation en position terminale est vérifiée par le compilateur.
- (def factorial
- (fn [n]
- (loop [cnt n acc 1]
- (if (zero? cnt)
- acc
- (recur (dec cnt) (* acc cnt))))))
(throw expr)
L'expression est évaluée et levée ; elle devrait donc produire une instance d'un dérivé de Throwable.
(try expr* catch-clause* finally-clause?)
|
catch-clause → (catch classname name expr*) finally-clause → (finally expr*) |
Les expressions sont évaluées et, en l'absence d'exception, la valeur de la dernière expression est renvoyée. En cas d'exception et si des clauses `catch` sont présentes, chacune est examinée successivement et la première pour laquelle l'exception levée est une instance de la classe est considérée comme une clause `catch` correspondante. Si une clause `catch` correspondante existe, ses expressions sont évaluées dans un contexte où le nom est lié à l'exception levée, et la valeur de la dernière est la valeur de retour de la fonction. En l'absence de clause `catch` correspondante, l'exception est propagée hors de la fonction. Avant tout retour, normal ou anormal, les effets de bord des expressions de la clause `finally` sont évalués.
(monitor-enter expr)
(monitor-exit expr)
Il s'agit de primitives de synchronisation à éviter dans le code utilisateur. Utilisez la macro de verrouillage.
Autres formes spéciales
Les formes spéciales point (« . »), nouveau et set! des champs sont décrites dans la section Interopérabilité Java de la documentation.
L'utilisation de set! sur les variables est décrite dans la section Variables de la documentation.
Formes de liaison (déstructuration)
La forme de liaison la plus simple en Clojure est un symbole. Cependant, Clojure prend également en charge la liaison structurelle abstraite appelée déstructuration dans les listes de liaison `let`, les listes de paramètres `fn` et, par extension, toute macro se développant en `let` ou `fn`. La déstructuration permet de créer un ensemble de liaisons à des valeurs d'une collection en utilisant une collection analogue comme forme de liaison. Une forme vectorielle spécifie les liaisons par position dans une collection séquentielle, une forme `map` par clé dans une collection associative. Les formes de déstructuration peuvent apparaître partout où les formes de liaison peuvent apparaître, et donc s'imbriquer, ce qui donne un code plus clair que l'utilisation d'accesseurs de collection.
Les formes de liaison ne correspondant pas à leur partie respective en raison d'une absence de données (par exemple, trop peu d'éléments dans une structure séquentielle, absence de clé dans une structure associative, etc.) sont liées à `nil`.
Déstructuration séquentielle
Les formes de liaison de vecteurs lient séquentiellement les valeurs des collections telles que les vecteurs, les listes, les séquences, les chaînes de caractères, les tableaux et tout type prenant en charge l'opérateur `nth`. La forme de déstructuration séquentielle est un vecteur de formes de liaison, qui seront liées aux éléments successifs de l'expression d'initialisation, accessibles via `nth`. De plus, et de manière optionnelle, une forme de liaison suivant un `&` sera liée au reste de la séquence, c'est-à-dire à la partie non encore liée, et accessible via `nthnext`.
Enfin, et également de manière optionnelle, `:as` suivi d'un symbole lie ce symbole à l'ensemble de l'expression d'initialisation :
- (let [[a b c & d :as e] [1 2 3 4 5 6 7]]
- [a b c d e])
-
- ->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]
Ces formulaires peuvent s'imbriquer :
- (let [[[x1 y1][x2 y2]] [[1 2] [3 4]]]
- [x1 y1 x2 y2])
-
- ->[1 2 3 4]
Dans tous les cas séquentiels, les formes de liaison dans la liaison de déstructuration correspondront aux emplacements dans la structure de données cible où résident les valeurs souhaitées.
Déstructuration associative
Les liaisons de type map créent des correspondances en recherchant des valeurs dans des collections telles que des maps, des ensembles, des vecteurs, des chaînes de caractères et des tableaux (ces trois derniers ayant des clés entières). Elles consistent en une map de paires liaison-forme→clef, chaque liaison étant associée à la valeur de l'expression d'initialisation correspondant à la clé fournie. De plus, et de manière optionnelle, une clé `:as` dans la liaison, suivie d'un symbole, lie ce symbole à l'expression d'initialisation entière. Également de manière optionnelle, une clé `:or` dans la liaison, suivie d'une autre map, peut être utilisée pour fournir des valeurs par défaut pour certaines ou toutes les clés si elles ne sont pas présentes dans l'expression d'initialisation.
- (let [{a :a, b :b, c :c, :as m :or {a 2 b 3}} {:a 5 :c 6}]
- [a b c m])
-
- ->[5 3 6 {:c 6, :a 5}]
Il arrive souvent que vous souhaitiez associer des symboles portant le même nom que les clés correspondantes de la table de correspondance. La directive `:keys` permet de résoudre la redondance fréquemment rencontrée dans les paires `binding-form→key`.
- (let [{fred :fred ethel :ethel lucy :lucy} m] ...
peut s'écrire :
- (let [{:keys [fred ethel lucy]} m] ...
À partir de Clojure 1.6, vous pouvez également utiliser des clefs de carte préfixées sous forme de déstructuration de carte :
- (let [m {:x/a 1, :y/b 2}
- {:keys [x/a y/b]} m]
- (+ a b))
-
- -> 3
Dans le cas de l'utilisation de clefs préfixées, le nom du symbole lié est identique à celui figurant à droite de la clef préfixée. Vous pouvez également utiliser des formes de mots clés à résolution automatique dans la directive :keys.
- (let [m {::x 42}
- {:keys [::x]} m]
- x)
-
- -> 42
Il existe des directives similaires `:strs` et `:syms` pour la correspondance des clefs de type chaîne et symbole. Depuis Clojure 1.6, `:syms` accepte également les clefs de symbole préfixées.
Clojure 1.9 introduit la prise en charge de la déstructuration directe de plusieurs clés (ou symboles) partageant le même espace de noms, grâce aux formes de déstructuration suivantes :
- :ns/keys - ns spécifie l'espace de noms par défaut pour la recherche de la clef dans l'entrée.
- Les éléments keys ne doivent pas spécifier d'espace de noms.
- Les éléments keys définissent également de nouveaux symboles locaux, comme avec :keys.
- :ns/syms - ns spécifie l'espace de noms par défaut pour la recherche du symbole dans l'entrée.
- Les éléments syms ne doivent pas spécifier d'espace de noms.
- Les éléments syms définissent également de nouveaux symboles locaux, comme avec :syms.
- (let [m #:domain{:a 1, :b 2}
- {:domain/keys [a b]} m]
- [a b])
-
- -> [1 2]
Arguments nommés
Les arguments nommés sont des arguments variadiques optionnels de fin de fonction, de la forme `akey aval bkey bval...`, accessibles dans le corps de la fonction par déstructuration associative. De plus, depuis Clojure 1.11, une fonction acceptant des arguments nommés peut recevoir une seule `map` à la place ou en plus (et après) des paires clef/valeur. Lorsqu'une seule `map` est passée, elle est utilisée directement pour la déstructuration ; sinon, une `map` est ajoutée à la `map` construite à partir des paires clef/valeur précédentes via `conj`. Pour définir une fonction acceptant des arguments nommés, il faut fournir une forme de déstructuration de `map` dans la déclaration `rest-param`. Par exemple, une fonction prenant une séquence et des arguments nommés optionnels et retournant un vecteur contenant les valeurs est définie comme suit :
- (defn destr [& {:keys [a b] :as opts}]
- [a b opts])
-
- (destr :a 1)
- ->[1 nil {:a 1}]
-
- (destr {:a 1 :b 2})
- ->[1 2 {:a 1 :b 2}]
La forme de liaison de type `map` à droite du `&` dans `destr` est une forme de liaison de déstructuration associative décrite précédemment.
Les deux déclarations de `foo` ci-dessous sont équivalentes, illustrant l'interprétation des séquences par déstructuration associative :
- (defn foo [& {:keys [quux]}] ...)
-
- (defn foo [& opts]
- (let [{:keys [quux]} opts] ...))
Déstructuration imbriquée
Comme les formes de liaison peuvent être imbriquées arbitrairement, il est possible de décomposer presque n'importe quoi :
- (let [m {:j 15 :k 16 :ivec [22 23 24 25]}
- {j :j, k :k, i :i, [r s & t :as v] :ivec, :or {i 12 j 13}} m]
- [i j k r s t v])
-
- -> [12 15 16 22 23 (24 25) [22 23 24 25]]