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

92

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.

fogus
la source
4
Juste pour info, pour les développeurs de Clojure qui recherchent java.util.Collection # contient une fonctionnalité de type, consultez clojure.contrib.seq-utils / includes? À partir de la documentation: Utilisation: (comprend? Coll x). Renvoie true si coll contient quelque chose égal (avec =) à x, en temps linéaire.
Robert Campbell
11
Vous semblez avoir manqué le fait que ces questions sont Wiki de la communauté
3
J'adore le décalage entre la question Perl et toutes les autres :)
Ether
8
Pour les développeurs de Clojure à la recherche de contenus, je recommanderais de ne pas suivre les conseils de rcampbell. seq-utils est depuis longtemps obsolète et cette fonction n'a jamais été utile au départ. Vous pouvez utiliser la somefonction de Clojure ou, mieux encore, simplement l'utiliser contains. Les collections Clojure sont implémentées java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Réponses:

70

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:

java.lang.NumberFormatException: Invalid number: 08

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:

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

donc je pensais que cela fonctionnerait également:

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

mais il échoue avec:

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

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

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

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

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

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:

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

tandis que Dan montre que vous pouvez utiliser la fonction d' identité pour dérouler la parenthèse extérieure:

(#(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étermine l' implémentation de la carte concrète immuable

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 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:

; 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, la boucle aurait été plus concise 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 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:

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 plus d'une expression de liaison, en itérant d'abord sur l'expression la plus à droite et en travaillant à 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 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:

(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 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:

(ns com.14clouds.myapp.repository)

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

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

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

|-- 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 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:

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

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.

rcampbell
la source
1
Toutes les macros de lecture ont une version fonctionnelle normale. Vous pourriez faire (#(hash-set %1 %2) :a 1)ou dans ce cas (hash-set :a 1).
Brian Carper
2
Vous pouvez également «supprimer» les parenthèses supplémentaires avec l'identité: (# (identity {% 1% 2}): a 1)
1
Vous pouvez également utiliser do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - Je n'aime pas autant cette solution que les précédentes car cela implique qu'un effet secondaire se produit, alors qu'en fait ce n'est pas le cas ici.
Robert Campbell
@ rrc7cz: Eh bien, en réalité, il n'est pas du tout nécessaire d'utiliser une fonction anonyme ici, puisque l'utilisation hash-mapdirecte (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 parfois dodans 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.
Michał Marczyk
42

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.

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

Le mapn'est jamais évalué, il est ignoré en silence, car il est paresseux. Vous devez utiliser l' un des doseq, dorun, doalletc. 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

Utiliser un nu mapau 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.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Brian Carper
la source
1
+1. Cela m'a mordu, mais d'une manière plus insidieuse: j'évaluais (map ...)de l'intérieur (binding ...)et je me demandais pourquoi les nouvelles valeurs de liaison ne s'appliquent pas.
Alex B
20

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 refs 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.

Carl Smotricz
la source
13

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.

Vagif Verdi
la source
8
Notez que (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!
Michał Marczyk
Cela semble fonctionner comme prévu dans Clojure 1.4.0: (assert (=: false (if Boolean / FALSE: true: false)))
Jakub Holý
J'ai également été brûlé par celui-ci récemment en faisant (filter: mykey coll) où: mykey's values ​​where Booleans - fonctionne comme prévu avec les collections créées par Clojure, mais PAS avec des collections désérialisées, lorsqu'elles sont sérialisées à l'aide de la sérialisation Java par défaut - parce que ces booléens sont désérialisés comme new Boolean (), et malheureusement (new Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon
1
Rappelez-vous simplement les règles de base des valeurs booléennes dans Clojure - nilet falsesont fausses, et tout le reste est vrai. Un Java Booleann'est pas nilet ne l'est pas false(car c'est un objet), donc le comportement est cohérent.
erikprice
13

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 'recuret 'trampolinedoit 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-seqou 'lazy-cons(ou en vous basant sur des API paresseuses de niveau supérieur), mais si vous l'enveloppez 'vecou 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.

Chris Vest
la source
4
la branche de développement à venir va un long chemin vers la réduction du premier élément en effaçant les références aux objets dans une fonction une fois qu'ils deviennent localement inaccessibles.
Arthur Ulfeldt
9

en utilisant loop ... recurpour traiter les séquences lorsque la carte fera l'affaire.

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

contre.

(map do-stuff data)

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".

Arthur Ulfeldt
la source
1
Ces deux versions ne sont en fait pas équivalentes. Votre workfonction équivaut à (doseq [item data] (do-stuff item)). (Outre le fait, cette boucle de travail ne se termine jamais.)
kotarak
oui, le premier brise la paresse sur ses arguments. le seq résultant aura les mêmes valeurs bien qu'il ne s'agisse plus d'un seq paresseux.
Arthur Ulfeldt
+1! J'ai écrit de nombreuses petites fonctions récursives uniquement pour trouver un autre jour où tout cela pourrait être généralisé en utilisant mapet / ou reduce.
nperson325681
5

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 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:

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"
Matt Fenwick
la source
3

trop de parantheses, en particulier avec l'appel de la méthode void java à l'intérieur qui aboutit à NPE:

public void foo() {}

((.foo))

résulte en NPE des parantheses externes parce que les parantheses internes ont une valeur nulle.

public int bar() { return 5; }

((.bar)) 

résulte dans le plus facile à déboguer:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
miaubiz
la source