web-dev-qa-db-fra.com

Erreurs de programmation courantes que les développeurs de Clojure doivent éviter

Quelles sont les erreurs courantes commises par les développeurs de Clojure et comment pouvons-nous les éviter?

Par exemple; les nouveaux venus à Clojure pensent que le contains? la fonction fonctionne de la même manière que Java.util.Collection#contains. Pourtant, contains? ne fonctionnera de la même manière que lorsqu'il est utilisé avec des collections indexées comme des cartes et des ensembles et vous recherchez une clé donnée:

(contains? {:a 1 :b 2} :b)
;=> true
(contains? {:a 1 :b 2} 2)
;=> false
(contains? #{:a 1 :b 2} :b)
;=> true

Lorsqu'il est utilisé avec des collections indexées numériquement (vecteurs, tableaux) contains?seulement vérifie que l'élément donné se trouve dans la plage d'index valide (base zéro):

(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true

Si on leur donne une liste, contains? ne reviendra jamais vrai.

93
fogus

Octaux littéraux

À un moment donné, je lisais dans une matrice qui utilisait des zéros non significatifs pour maintenir des lignes et des colonnes appropriées. Mathématiquement, c'est correct, car le zéro devant ne modifie évidemment pas la valeur sous-jacente. Cependant, les tentatives pour définir un var avec cette matrice échoueraient mystérieusement avec:

Java.lang.NumberFormatException: Invalid number: 08

ce qui m'a totalement dérouté. La raison en est que Clojure traite les valeurs entières littérales avec des zéros en tête comme des octaux, et il n'y a pas de numéro 08 en octal.

Je dois également mentionner que Clojure prend en charge les valeurs traditionnelles Java valeurs hexadécimales via le préfixe 0x . Vous pouvez également utiliser n'importe quelle base entre 2 et 36 en utilisant la notation "base + r + valeur", comme 2r101010 ou 36r16 qui sont 42 base dix.


Essayer de renvoyer des littéraux dans un littéral de fonction anonyme

Cela marche:

user> (defn foo [key val]
    {key val})
#'user/foo
user> (foo :a 1)
{:a 1}

donc je pensais que cela fonctionnerait aussi:

(#({%1 %2}) :a 1)

mais il échoue avec:

Java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap

car la macro de lecteur # () est étendue à

(fn [%1 %2] ({%1 %2}))  

avec la carte littérale enveloppée entre parenthèses. Comme il s'agit du premier élément, il est traité comme une fonction (qui est en fait une carte littérale), mais aucun argument requis (comme une clé) n'est fourni. En résumé, le littéral de la fonction anonyme ne pas se développe en

(fn [%1 %2] {%1 %2})  ; notice the lack of parenthesis

et vous ne pouvez donc pas avoir de valeur littérale ([],: a, 4,%) comme corps de la fonction anonyme.

Deux solutions ont été proposées dans les commentaires. Brian Carper suggère d'utiliser des constructeurs d'implémentation de séquence (array-map, hash-set, vector) comme ceci:

(#(array-map %1 %2) :a 1)

tandis que Dan montre que vous pouvez utiliser la fonction identité pour déballer la parenthèse externe:

(#(identity {%1 %2}) :a 1)

La suggestion de Brian m'amène en fait à ma prochaine erreur ...


Penser que hash-map ou array-map déterminer la immuable implémentation concrète de la carte

Considérer ce qui suit:

user> (class (hash-map))
clojure.lang.PersistentArrayMap
user> (class (hash-map :a 1))
clojure.lang.PersistentHashMap
user> (class (assoc (apply array-map (range 2000)) :a :1))
clojure.lang.PersistentHashMap

Bien que vous n'ayez généralement pas à vous soucier de la mise en œuvre concrète d'une carte Clojure, vous devez savoir que les fonctions qui développent une carte - comme assoc ou conj - peut prendre un PersistentArrayMap et retourner un PersistentHashMap, qui fonctionne plus rapidement pour des cartes plus grandes.


Utiliser une fonction comme point de récursivité plutôt qu'une boucle pour fournir les liaisons initiales

Quand j'ai commencé, j'ai écrit beaucoup de fonctions comme ceci:

; Project Euler #3
(defn p3 
  ([] (p3 775147 600851475143 3))
  ([i n times]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

Alors qu'en fait boucle aurait été plus concis et idiomatique pour cette fonction particulière:

; Elapsed time: 387 msecs
(defn p3 [] {:post [(= % 6857)]}
  (loop [i 775147 n 600851475143 times 3]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

Notez que j'ai remplacé l'argument vide, corps de fonction "constructeur par défaut" (p3 775147 600851475143 3) par une boucle + liaison initiale. La récurrence lie désormais les liaisons de boucle (au lieu des paramètres fn) et revient au point de récursivité (boucle, au lieu de fn).


Référencement des vars "fantômes"

Je parle du type de var que vous pourriez définir en utilisant le REPL - pendant votre programmation exploratoire - puis référencez inconsciemment dans votre source. Tout fonctionne bien jusqu'à ce que vous rechargiez l'espace de noms (peut-être en fermant votre éditeur) et découvrir plus tard un tas de symboles non liés référencés dans votre code. Cela se produit également fréquemment lorsque vous refactorisez, en déplaçant un var d'un espace de noms à un autre.


Traiter le for compréhension de la liste comme un impératif pour la boucle

Essentiellement, vous créez une liste paresseuse basée sur des listes existantes plutôt que de simplement effectuer une boucle contrôlée. doseq de Clojure est en fait plus analogue aux constructions impératives pour chaque boucle.

Un exemple de leur différence est la possibilité de filtrer les éléments sur lesquels ils itèrent en utilisant des prédicats arbitraires:

user> (for [n '(1 2 3 4) :when (even? n)] n)
(2 4)

user> (for [n '(4 3 2 1) :while (even? n)] n)
(4)

Une autre façon dont ils sont différents est qu'ils peuvent fonctionner sur des séquences paresseuses infinies:

user> (take 5 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))
(4 6 8 10 12)

Ils peuvent également gérer plusieurs expressions de liaison, itérant d'abord sur l'expression la plus à droite et poursuivant vers la gauche:

user> (for [x '(1 2 3) y '(\a \b \c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")

Il n'y a pas non plus pause ou continuez pour quitter prématurément.


Surutilisation des structures

Je viens d'un milieu OOPish alors quand j'ai commencé Clojure, mon cerveau pensait toujours en termes d'objets. Je me suis retrouvé à tout modéliser comme une structure parce que son regroupement de "membres", aussi lâche, me faisait me sentir à l'aise. En réalité, les structures doivent être considérées comme une optimisation; Clojure partagera les clés et certaines informations de recherche pour conserver la mémoire. Vous pouvez les optimiser davantage en définissant accesseurs pour accélérer le processus de recherche de clés.

Globalement, vous ne gagnez rien à utiliser une structure sur une carte sauf pour les performances, la complexité supplémentaire pourrait ne pas en valoir la peine.


Utilisation de constructeurs BigDecimal non incrustés

J'avais besoin de beaucoup de BigDecimals et j'écrivais du code laid comme ceci:

(let [foo (BigDecimal. "1") bar (BigDecimal. "42.42") baz (BigDecimal. "24.24")]

alors qu'en fait Clojure prend en charge les littéraux BigDecimal en ajoutant [~ # ~] m [~ # ~] au nombre:

(= (BigDecimal. "42.42") 42.42M) ; true

L'utilisation de la version sucrée élimine une grande partie du ballonnement. Dans les commentaires, twils a mentionné que vous pouvez également utiliser les fonctions bigdec et bigint pour plus explicite, tout en restant concis.


Utilisation du Java conversions de nommage des packages pour les espaces de noms

Ce n'est pas en fait une erreur en soi, mais plutôt quelque chose qui va à l'encontre de la structure idiomatique et de la dénomination d'un projet Clojure typique. Mon premier projet Clojure substantiel avait des déclarations d'espace de noms - et des structures de dossiers correspondantes - comme ceci:

(ns com.14clouds.myapp.repository)

qui a gonflé mes références de fonctions pleinement qualifiées:

(com.14clouds.myapp.repository/load-by-name "foo")

Pour compliquer encore les choses, j'ai utilisé une structure de répertoires standard Maven :

|-- src/
|   |-- main/
|   |   |-- Java/
|   |   |-- clojure/
|   |   |-- resources/
|   |-- test/
...

qui est plus complexe que la structure Clojure "standard" de:

|-- src/
|-- test/
|-- resources/

qui est la valeur par défaut de Leiningen projets et Clojure lui-même.


Les cartes utilisent les égales de Java () plutôt que les Clojure = pour la correspondance des clés

Initialement rapporté par chouser le IRC , cette utilisation de Java égal à () conduit à des résultats peu intuitifs:

user> (= (int 1) (long 1))
true
user> ({(int 1) :found} (int 1) :not-found)
:found
user> ({(int 1) :found} (long 1) :not-found)
:not-found

Étant donné que Entier et Les instances longues de 1 sont imprimées de la même manière par défaut , il peut être difficile de détecter pourquoi votre carte ne renvoie aucune valeur. Cela est particulièrement vrai lorsque vous passez votre clé via une fonction qui, peut-être à votre insu, renvoie un long.

Il convient de noter que l'utilisation de Java équivaut à () au lieu de Clojure = est essentiel pour que les cartes soient conformes à l'interface Java.util.Map.


J'utilise Programmation Clojure par Stuart Halloway, Practical Clojure par Luke VanderHart, et l'aide d'innombrables pirates de Clojure sur IRC et la liste de diffusion pour m'aider dans mes réponses.

71
Robert Campbell

Oublier de forcer l'évaluation des séquences paresseuses

Les séquences paresseuses ne sont pas évaluées, sauf si vous leur demandez d'être évaluées. Vous pouvez vous attendre à ce que cela imprime quelque chose, mais ce n'est pas le cas.

user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil

Le map n'est jamais évalué, il est ignoré en silence, car il est paresseux. Vous devez utiliser l'un des doseq, dorun, doall etc. pour forcer l'évaluation des séquences paresseuses pour les effets secondaires.

user=> (defn foo [] (doseq [x [:foo :bar]] (println x)) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil
user=> (defn foo [] (dorun (map println [:foo :bar])) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil

L'utilisation d'un map nu au type REPL ressemble à cela fonctionne, mais cela ne fonctionne que parce que le REPL force l'évaluation des séquences paresseuses lui-même) Cela peut rendre le bogue encore plus difficile à remarquer, car votre code fonctionne à REPL et ne fonctionne pas à partir d'un fichier source ou à l'intérieur d'une fonction.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
42
Brian Carper

Je suis un Noo Clojure. Les utilisateurs plus avancés peuvent avoir des problèmes plus intéressants.

essayer d'imprimer des séquences paresseuses infinies.

Je savais ce que je faisais avec mes séquences paresseuses, mais à des fins de débogage, j'ai inséré des appels print/prn/pr, ayant temporairement oublié ce que j'étais en train d'imprimer. Drôle, pourquoi mon PC a-t-il tous raccroché?

essayer de programmer Clojure impérativement.

Il y a une certaine tentation de créer beaucoup de refs ou atoms et d'écrire du code qui débloque constamment avec leur état. Cela peut être fait, mais ce n'est pas un bon ajustement. Il peut également avoir des performances médiocres et bénéficier rarement de plusieurs cœurs.

essayer de programmer Clojure à 100% de manière fonctionnelle.

Un revers de la médaille: certains algorithmes veulent vraiment un peu d'état mutable. Éviter religieusement l'état mutable à tout prix peut entraîner des algorithmes lents ou maladroits. Il faut du jugement et un peu d'expérience pour prendre la décision.

essayer d'en faire trop en Java.

Parce qu'il est si facile d'atteindre Java, il est parfois tentant d'utiliser Clojure comme wrapper de langage de script autour de Java. Vous devrez certainement faire exactement cela lorsque vous utilisez la fonctionnalité de bibliothèque Java, mais il n'y a pas de sens (par exemple) à maintenir les structures de données en Java ou à utiliser Java types de données tels que les collections pour lesquelles il existe de bons équivalents dans Clojure.

21
Carl Smotricz

Garder la tête en boucle.
Vous risquez de manquer de mémoire si vous parcourez les éléments d'une séquence paresseuse potentiellement très grande ou infinie tout en conservant une référence au premier élément.

Oublier qu'il n'y a pas de TCO.
Les appels de queue réguliers consomment de l'espace de pile et ils débordent si vous ne faites pas attention. Clojure a 'recur et 'trampoline pour gérer de nombreux cas où des appels de queue optimisés seraient utilisés dans d'autres langues, mais ces techniques doivent être intentionnellement appliquées.

séquences pas assez paresseuses.
Vous pouvez créer une séquence paresseuse avec 'lazy-seq ou 'lazy-cons (ou en s'appuyant sur des API paresseuses de niveau supérieur), mais si vous l'enveloppez dans 'vec ou le passer à travers une autre fonction qui réalise la séquence, alors il ne sera plus paresseux. La pile et le tas peuvent être survolés par cela.

Mettre des choses mutables dans les références.
Vous pouvez techniquement le faire, mais seule la référence d'objet dans la référence elle-même est régie par la STM - pas l'objet référé et ses champs (sauf s'ils sont immuables et pointent vers d'autres références). Donc, dans la mesure du possible, préférez uniquement les objets immuables dans les références. La même chose vaut pour les atomes.

13
Chris Vest

Beaucoup de choses déjà mentionnées. Je vais juste en ajouter un de plus.

Clojure if traite Java Les objets booléens sont toujours aussi vrais même si sa valeur est fausse. Donc, si vous avez une fonction Java land qui renvoie un Java valeur booléenne, assurez-vous de ne pas le vérifier directement (if Java-bool "Yes" "No") mais plutôt (if (boolean Java-bool) "Yes" "No").

J'ai été brûlé par cela avec la bibliothèque clojure.contrib.sql qui renvoie les champs booléens de la base de données sous la forme Java objets booléens.

13
Vagif Verdi

en utilisant loop ... recur pour traiter les séquences quand la carte fera l'affaire.

(defn work [data]
    (do-stuff (first data))
    (recur (rest data)))

vs.

(map do-stuff data)

La fonction de carte (dans la dernière branche) utilise des séquences fragmentées et de nombreuses autres optimisations. De plus, comme cette fonction est fréquemment exécutée, le Hotspot JIT est généralement optimisé et prêt à fonctionner sans aucun "temps de préchauffage".

9
Arthur Ulfeldt

Les types de collection ont des comportements différents pour certaines opérations:

user=> (conj '(1 2 3) 4)    
(4 1 2 3)                 ;; new element at the front
user=> (conj [1 2 3] 4) 
[1 2 3 4]                 ;; new element at the back

user=> (into '(3 4) (list 5 6 7))
(7 6 5 3 4)
user=> (into [3 4] (list 5 6 7)) 
[3 4 5 6 7]

Travailler avec des chaînes peut être déroutant (je ne les comprends toujours pas). Plus précisément, les chaînes ne sont pas les mêmes que les séquences de caractères, même si les fonctions de séquence fonctionnent sur elles:

user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)

Pour récupérer une chaîne, vous devez faire:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
5
Matt Fenwick

trop de parenthèses, en particulier avec void Java appel de méthode à l'intérieur qui se traduit par NPE:

public void foo() {}

((.foo))

résulte en NPE des parantheses externes car les parantheses internes sont nulles.

public int bar() { return 5; }

((.bar)) 

permet de déboguer plus facilement:

Java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class Java.lang.ClassCastException]
3
miaubiz