Quelles sont les erreurs courantes commises par les développeurs de Clojure et comment pouvons-nous les éviter?
Par exemple; les nouveaux venus chez Clojure pensent que la contains?
fonction fonctionne de la même manière que java.util.Collection#contains
. Cependant, contains?
ne fonctionnera de la même manière que lorsqu'il est utilisé avec des collections indexées telles que des cartes et des ensembles et que 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) vérifie contains?
uniquement que l'élément donné est dans la plage d'index valide (base zéro):
(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true
Si une liste est donnée, contains?
ne retournera jamais true.
some
fonction de Clojure ou, mieux encore, simplement l'utilisercontains
. Les collections Clojure sont implémentéesjava.util.Collection
.(.contains [1 2 3] 2) => true
Réponses:
Octales littérales
À un moment donné, je lisais une matrice qui utilisait des zéros non significatifs pour maintenir les lignes et les colonnes appropriées. Mathématiquement, cela est correct, car le zéro non significatif ne modifie évidemment pas la valeur sous-jacente. Les tentatives pour définir une variable avec cette matrice, cependant, échoueraient mystérieusement avec:
ce qui m'a totalement déconcerté. La raison en est que Clojure traite les valeurs entières littérales avec des zéros non significatifs comme des octals, et il n'y a pas de nombre 08 en octal.
Je dois également mentionner que Clojure prend en charge les valeurs hexadécimales Java traditionnelles via le préfixe 0x . Vous pouvez également utiliser n'importe quelle base entre 2 et 36 en utilisant la notation «base + r + valeur», telle que 2r101010 ou 36r16 qui sont 42 base dix.
Essayer de renvoyer des littéraux dans un littéral de fonction anonyme
Cela marche:
donc je pensais que cela fonctionnerait également:
mais il échoue avec:
car la macro de lecture # () est étendue à
avec le littéral de la carte entre parenthèses. Puisqu'il s'agit du premier élément, il est traité comme une fonction (ce qu'une carte littérale est en fait), mais aucun argument requis (comme une clé) n'est fourni. En résumé, le littéral de fonction anonyme ne se développe pas en
et donc vous ne pouvez 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:
tandis que Dan montre que vous pouvez utiliser la fonction d' identité pour dérouler la parenthèse extérieure:
La suggestion de Brian m'amène en fait à ma prochaine erreur ...
Penser que hash-map ou array-map détermine l' implémentation de la carte concrète immuable
Considérer ce qui suit:
Bien que vous n'ayez généralement pas à vous soucier de l'implémentation concrète d'une carte Clojure, vous devez savoir que les fonctions qui développent une carte - comme assoc ou conj - peuvent prendre un PersistentArrayMap et retourner un PersistentHashMap , qui fonctionne plus rapidement pour les 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 celle-ci:
Alors qu'en fait, la boucle aurait été plus concise et idiomatique pour cette fonction particulière:
Notez que j'ai remplacé l'argument vide, corps de la fonction "constructeur par défaut" (p3 775147 600851475143 3) par une boucle + liaison initiale. La RECUR lie à nouveau maintenant 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 variables "fantômes"
Je parle du type de var que vous pourriez définir en utilisant le REPL - lors de votre programmation exploratoire - puis référence sans le savoir dans votre source. Tout fonctionne bien jusqu'à ce que vous rechargiez l'espace de noms (peut-être en fermant votre éditeur) et que vous découvriez plus tard un tas de symboles non liés référencés dans votre code. Cela se produit également fréquemment lorsque vous refactorez, en déplaçant une variable d'un espace de noms à un autre.
Traiter la compréhension de la liste for comme une boucle for impérative
Essentiellement, vous créez une liste différée 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 de boucle foreach impératives.
Un exemple de leur différence est la possibilité de filtrer les éléments sur lesquels ils itèrent à l'aide de prédicats arbitraires:
Une autre façon dont ils sont différents est qu'ils peuvent fonctionner sur des séquences paresseuses infinies:
Ils peuvent également gérer plus d'une expression de liaison, en itérant d'abord sur l'expression la plus à droite et en travaillant à gauche:
Il n'y a pas non plus de pause ou de continuer à sortir prématurément.
Surutilisation des structures
Je viens d'un milieu OOPish donc quand j'ai commencé Clojure mon cerveau pensait toujours en termes d'objets. Je me suis retrouvé à modéliser tout comme une structure parce que son regroupement de «membres», même lâche, me mettait à 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 économiser la mémoire. Vous pouvez les optimiser davantage en définissant des accesseurs pour accélérer le processus de recherche de clé.
Dans l'ensemble, vous ne gagnez rien à utiliser une structure sur une carte, sauf pour les performances, donc la complexité supplémentaire pourrait ne pas en valoir la peine.
Utilisation de constructeurs BigDecimal non compatibles
J'avais besoin de beaucoup de BigDecimals et j'écrivais du code moche comme celui-ci:
alors qu'en fait Clojure prend en charge les littéraux BigDecimal en ajoutant M au nombre:
L'utilisation de la version sucrée élimine une grande partie des ballonnements. Dans les commentaires, twils a mentionné que vous pouvez également utiliser les fonctions bigdec et bigint pour être plus explicite, tout en restant concis.
Utilisation des conversions de noms de package Java 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:
qui a gonflé mes références de fonction pleinement qualifiées:
Pour compliquer encore plus les choses, j'ai utilisé une structure de répertoires Maven standard :
qui est plus complexe que la structure Clojure "standard" de:
qui est la valeur par défaut des projets Leiningen et Clojure lui-même.
Les cartes utilisent l'égalité de Java () plutôt que celle de Clojure = pour la correspondance des clés
Initialement rapporté par chouser sur IRC , cette utilisation de equals () de Java conduit à des résultats peu intuitifs:
Depuis à la fois Integer et Long instances 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 equals () de Java au lieu de = de Clojure est essentielle pour que les cartes se conforment à l'interface java.util.Map.
J'utilise Programming Clojure de Stuart Halloway, Practical Clojure de Luke VanderHart et l'aide d'innombrables hackers Clojure sur IRC et la liste de diffusion pour m'aider dans mes réponses.
la source
(#(hash-set %1 %2) :a 1)
ou dans ce cas(hash-set :a 1)
.do
:(#(do {%1 %2}) :a 1)
.hash-map
directe (comme dans(hash-map :a 1)
ou(map hash-map keys vals)
) est plus lisible et n'implique pas que quelque chose de spécial et non encore implémenté dans une fonction nommée est en cours (ce que l'utilisation de#(...)
implique, je trouve). En fait, la surutilisation des fns anonymes est un piège à penser en soi. :-) OTOH, j'utilise parfoisdo
dans des fonctions anonymes super concises sans effets secondaires ... Il semble évident qu'elles sont d'un seul coup d'œil. Une question de goût, je suppose.Oublier de forcer l'évaluation des seq paresseux
Les séquences paresseuses ne sont pas évaluées à moins que vous ne leur demandiez d'être évaluées. Vous pourriez vous attendre à ce que cela imprime quelque chose, mais ce n'est pas le cas.
Le
map
n'est jamais évalué, il est ignoré en silence, car il est paresseux. Vous devez utiliser l' un desdoseq
,dorun
,doall
etc. pour forcer l' évaluation des séquences paresseuses pour les effets secondaires.Utiliser un nu
map
au REPL semble fonctionner, mais cela ne fonctionne que parce que le REPL force l'évaluation des seq paresseux lui-même. Cela peut rendre le bogue encore plus difficile à remarquer, car votre code fonctionne à la REPL et ne fonctionne pas à partir d'un fichier source ou à l'intérieur d'une fonction.la source
(map ...)
de l'intérieur(binding ...)
et je me demandais pourquoi les nouvelles valeurs de liaison ne s'appliquent pas.Je suis un noob Clojure. Les utilisateurs plus avancés peuvent avoir des problèmes plus intéressants.
essayant 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'imprimais. C'est drôle, pourquoi mon PC a-t-il tout raccroché?
essayer de programmer Clojure impérativement.
Il y a une certaine tentation de créer beaucoup de
ref
s ouatom
s et d'écrire du code qui se moque constamment de leur état. Cela peut être fait, mais ce n'est pas une bonne solution. 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 gênants. Il faut du jugement et un peu d'expérience pour prendre une 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 utiliserez la fonctionnalité de la bibliothèque Java, mais cela n'a guère de sens (par exemple) de maintenir des structures de données en Java, ou d'utiliser des types de données Java tels que des collections pour lesquelles il existe de bons équivalents dans Clojure.
la source
Beaucoup de choses déjà mentionnées. Je vais juste en ajouter un de plus.
Clojure if traite les objets Java Boolean toujours comme true même si sa valeur est false. Donc, si vous avez une fonction java land qui renvoie une valeur booléenne java, assurez-vous de ne pas la 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 en tant qu'objets booléens java.
la source
(if java.lang.Boolean/FALSE (println "foo"))
cela n'imprime pas foo.(if (java.lang.Boolean. "false") (println "foo"))
fait, cependant, alors(if (boolean (java.lang.Boolean "false")) (println "foo"))
que non ... C'est vraiment déroutant!nil
etfalse
sont fausses, et tout le reste est vrai. Un JavaBoolean
n'est pasnil
et ne l'est pasfalse
(car c'est un objet), donc le comportement est cohérent.Garder la tête en boucle.
Vous risquez de manquer de mémoire si vous bouclez sur les éléments d'une séquence paresseuse potentiellement très grande, voire infinie, tout en gardant une référence au premier élément.
Oubliant qu'il n'y a pas de TCO.
Les appels de queue réguliers consomment de l'espace dans la pile, et ils déborderont si vous ne faites pas attention. Clojure a
'recur
et'trampoline
doit gérer de nombreux cas où des appels de queue optimisés seraient utilisés dans d'autres langages, mais ces techniques doivent être appliquées intentionnellement.Séquences pas tout à fait paresseuses.
Vous pouvez créer une séquence paresseuse avec
'lazy-seq
ou'lazy-cons
(ou en vous basant sur des API paresseuses de niveau supérieur), mais si vous l'enveloppez'vec
ou la passez par une autre fonction qui réalise la séquence, elle ne sera plus paresseuse. La pile et le tas peuvent être survolés par cela.Mettre des choses mutables dans des refs.
Vous pouvez le faire techniquement, mais seule la référence d'objet dans la référence elle-même est régie par le STM - pas l'objet référencé et ses champs (à moins qu'ils ne soient immuables et pointent vers d'autres références). Dans la mesure du possible, préférez uniquement les objets immuables dans les références. Il en va de même pour les atomes.
la source
en utilisant
loop ... recur
pour traiter les séquences lorsque la carte fera l'affaire.contre.
La fonction map (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 l'a généralement optimisée et prête à fonctionner sans aucun "temps de préchauffage".
la source
work
fonction équivaut à(doseq [item data] (do-stuff item))
. (Outre le fait, cette boucle de travail ne se termine jamais.)map
et / oureduce
.Les types de collection ont des comportements différents pour certaines opérations:
Travailler avec des chaînes peut être déroutant (je ne les comprends toujours pas tout à fait). 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:
Pour récupérer une chaîne, vous devez faire:
la source
trop de parantheses, en particulier avec l'appel de la méthode void java à l'intérieur qui aboutit à NPE:
résulte en NPE des parantheses externes parce que les parantheses internes ont une valeur nulle.
résulte dans le plus facile à déboguer:
la source