Errori di programmazione comuni che gli sviluppatori Clojure devono evitare [chiuso]


92

Quali sono alcuni errori comuni commessi dagli sviluppatori Clojure e come possiamo evitarli?

Per esempio; i nuovi arrivati ​​a Clojure pensano che la contains?funzione funzioni allo stesso modo di java.util.Collection#contains. Tuttavia, contains?funzionerà in modo simile solo se utilizzato con raccolte indicizzate come mappe e set e stai cercando una determinata chiave:

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

Quando viene utilizzato con raccolte indicizzate numericamente (vettori, array) controlla contains? solo che l'elemento specificato sia all'interno dell'intervallo di indici valido (in base zero):

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

Se viene fornito un elenco, contains?non restituirà mai vero.


4
FYI, per quegli sviluppatori di Clojure che cercano java.util.Collection # contiene funzionalità di tipo, controlla clojure.contrib.seq-utils / includes? Dalla documentazione: Utilizzo: (include? Coll x). Restituisce vero se coll contiene qualcosa di uguale (con =) ax, in tempo lineare.
Robert Campbell

11
Sembra che tu abbia perso il fatto che tali domande sono Wiki

3
Adoro il modo in cui la domanda Perl deve essere fuori passo rispetto a tutte le altre :)
Ether

8
Per gli sviluppatori di Clojure che cercano contiene, consiglierei di non seguire il consiglio di rcampbell. seq-utils è stato deprecato da tempo e quella funzione non è mai stata utile all'inizio. Puoi usare la somefunzione di Clojure o, meglio ancora, usare solo containsse stesso. Le collezioni Clojure implementano java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Risposte:


70

Ottali letterali

A un certo punto stavo leggendo in una matrice che utilizzava gli zeri iniziali per mantenere righe e colonne corrette. Matematicamente questo è corretto, poiché lo zero iniziale ovviamente non altera il valore sottostante. I tentativi di definire una var con questa matrice, tuttavia, fallirebbero misteriosamente con:

java.lang.NumberFormatException: Invalid number: 08

il che mi ha totalmente sconcertato. Il motivo è che Clojure tratta i valori interi letterali con zeri iniziali come ottali e non c'è il numero 08 in ottale.

Dovrei anche menzionare che Clojure supporta i tradizionali valori esadecimali Java tramite il prefisso 0x . Puoi anche usare qualsiasi base compresa tra 2 e 36 usando la notazione "base + r + valore", come 2r101010 o 36r16 che sono 42 base dieci.


Tentativo di restituire letterali in una funzione letterale anonima

Funziona:

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

quindi ho pensato che avrebbe funzionato anche:

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

ma fallisce con:

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

perché la macro del lettore # () viene espansa a

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

con il letterale della mappa racchiuso tra parentesi. Poiché è il primo elemento, viene trattato come una funzione (che in realtà è una mappa letterale), ma non vengono forniti argomenti obbligatori (come una chiave). In sintesi, la funzione letterale anonima non si espande in

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

e quindi non puoi avere alcun valore letterale ([],: a, 4,%) come corpo della funzione anonima.

Due soluzioni sono state fornite nei commenti. Brian Carper suggerisce di utilizzare costruttori di implementazione della sequenza (array-map, hash-set, vector) in questo modo:

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

mentre Dan mostra che puoi usare la funzione di identità per scartare la parentesi esterna:

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

Il suggerimento di Brian in realtà mi porta al mio prossimo errore ...


Pensare che hash-map o array-map determini l' implementazione della mappa concreta immutabile

Considera quanto segue:

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

Sebbene in genere non dovrai preoccuparti dell'implementazione concreta di una mappa Clojure, dovresti sapere che le funzioni che fanno crescere una mappa, come assoc o conj , possono prendere una PersistentArrayMap e restituire una PersistentHashMap , che funziona più velocemente per mappe più grandi.


Usare una funzione come punto di ricorsione piuttosto che un ciclo per fornire i collegamenti iniziali

Quando ho iniziato, ho scritto molte funzioni come questa:

; 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))))

Quando in effetti il ciclo sarebbe stato più conciso e idiomatico per questa particolare funzione:

; 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))))

Si noti che ho sostituito l'argomento vuoto, corpo della funzione "costruttore predefinito" (p3 775147 600851475143 3) con un ciclo + associazione iniziale. Il ripresentarsi ora rebinds binding ciclo (invece dei parametri fn) e ritorna al punto ricorsione (anello, anziché fn).


Riferimento a vars "fantasma"

Sto parlando del tipo di var che potresti definire usando REPL - durante la tua programmazione esplorativa - quindi riferimento inconsapevolmente nel tuo sorgente. Tutto funziona bene fino a quando non ricarichi lo spazio dei nomi (magari chiudendo l'editor) e in seguito scopri una serie di simboli non associati a cui fa riferimento il codice. Ciò accade spesso anche durante il refactoring, spostando una var da uno spazio dei nomi a un altro.


Trattare la comprensione della lista for come un ciclo for imperativo

In sostanza stai creando un elenco pigro basato su elenchi esistenti piuttosto che eseguire semplicemente un ciclo controllato. La doseq di Clojure è in realtà più analoga ai costrutti di ciclo foreach imperativi.

Un esempio di come sono diversi è la capacità di filtrare gli elementi su cui ripetono utilizzando predicati arbitrari:

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)

Un altro modo in cui sono diversi è che possono operare su infinite sequenze pigre:

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

Possono anche gestire più di un'espressione vincolante, iterando prima sull'espressione più a destra e procedendo a sinistra:

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

Inoltre, non ci sono interruzioni o si continua a uscire prematuramente.


Uso eccessivo di strutture

Vengo da un background OOP, quindi quando ho iniziato a Clojure il mio cervello stava ancora pensando in termini di oggetti. Mi sono ritrovato a modellare tutto come una struttura perché il suo raggruppamento di "membri", per quanto sciolto, mi faceva sentire a mio agio. In realtà, gli struct dovrebbero essere considerati principalmente un'ottimizzazione; Clojure condividerà le chiavi e alcune informazioni di ricerca per conservare la memoria. È possibile ottimizzarli ulteriormente definendo le funzioni di accesso per accelerare il processo di ricerca delle chiavi.

Nel complesso non si guadagna nulla dall'utilizzo di una struttura su una mappa tranne che per le prestazioni, quindi la complessità aggiuntiva potrebbe non valerne la pena.


Utilizzo di costruttori BigDecimal non zuccherati

Avevo bisogno di molti BigDecimals e stavo scrivendo un codice brutto come questo:

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

quando infatti Clojure supporta i letterali BigDecimal aggiungendo M al numero:

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

Usare la versione zuccherata elimina molto il gonfiore. Nei commenti, twils ha menzionato che puoi anche usare le funzioni bigdec e bigint per essere più espliciti, ma rimanere concisi.


Utilizzo delle conversioni di denominazione del pacchetto Java per gli spazi dei nomi

Questo non è in realtà un errore di per sé, ma piuttosto qualcosa che va contro la struttura idiomatica e la denominazione di un tipico progetto Clojure. Il mio primo sostanziale progetto Clojure aveva dichiarazioni dello spazio dei nomi e strutture di cartelle corrispondenti, come questa:

(ns com.14clouds.myapp.repository)

che ha gonfiato i miei riferimenti di funzione pienamente qualificati:

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

Per complicare ancora di più le cose, ho usato una struttura di directory Maven standard :

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

che è più complessa della struttura Clojure "standard" di:

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

che è l'impostazione predefinita dei progetti di Leiningen e della stessa Clojure .


Le mappe utilizzano Java equals () anziché Clojure = per la corrispondenza delle chiavi

Originariamente riportato da chouser su IRC , questo utilizzo di Java equals () porta ad alcuni risultati non intuitivi:

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

Poiché entrambe le istanze Intero e Lungo di 1 vengono stampate allo stesso modo per impostazione predefinita, può essere difficile rilevare il motivo per cui la mappa non restituisce alcun valore. Ciò è particolarmente vero quando passi la tua chiave attraverso una funzione che, forse a tua insaputa, restituisce un long.

Va notato che l'utilizzo di Java equals () invece di Clojure = è essenziale affinché le mappe siano conformi all'interfaccia java.util.Map.


Sto usando Programming Clojure di Stuart Halloway, Practical Clojure di Luke VanderHart e l'aiuto di innumerevoli hacker Clojure su IRC e la mailing list per aiutare con le mie risposte.


1
Tutte le macro del lettore hanno una normale versione delle funzioni. Potresti fare (#(hash-set %1 %2) :a 1)o in questo caso (hash-set :a 1).
Brian Carper

2
Puoi anche "rimuovere" le parentesi aggiuntive con identità: (# (identità {% 1% 2}): a 1)

1
Si potrebbe anche usare do: (#(do {%1 %2}) :a 1).
Michał Marczyk

@ Michał - non mi piace questa soluzione tanto quanto i precedenti, perché do implica che un effetto collaterale è in atto, quando in realtà questo non è il caso qui.
Robert Campbell

@ rrc7cz: Beh, in realtà, non c'è affatto bisogno di usare una funzione anonima qui, poiché l'uso hash-mapdiretto (come in (hash-map :a 1)o (map hash-map keys vals)) è più leggibile e non implica che qualcosa di speciale e non ancora implementato in una funzione con nome sta avvenendo (cosa che l'uso di #(...)implica, trovo). In effetti, un uso eccessivo di fns anonimi è un trucco a cui pensare in sé. :-) OTOH, a volte uso dofunzioni anonime molto concise che sono prive di effetti collaterali ... Tende ad essere ovvio che siano a colpo d'occhio. Una questione di gusti, immagino.
Michał Marczyk

42

Dimenticando di forzare la valutazione delle sequenze pigre

Le sequenze pigre non vengono valutate a meno che tu non chieda loro di essere valutate. Potresti aspettarti che questo stampi qualcosa, ma non è così.

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

Non mapviene mai valutato, viene silenziosamente scartato, perché è pigro. È necessario utilizzare uno dei doseq, dorun, doallecc per forzare la valutazione di sequenze pigri per effetti collaterali.

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'utilizzo di un bare mapal tipo REPL sembra funzionare, ma funziona solo perché REPL forza la valutazione delle sequenze pigre stesse. Questo può rendere il bug ancora più difficile da notare, perché il tuo codice funziona in REPL e non funziona da un file sorgente o all'interno di una funzione.

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

1
+1. Questo mi ha morso, ma in un modo più insidioso: stavo valutando (map ...)dall'interno (binding ...)e chiedendomi perché i nuovi valori vincolanti non si applicano.
Alex B

20

Sono un noob Clojure. Gli utenti più avanzati potrebbero avere problemi più interessanti.

cercando di stampare infinite sequenze pigre.

Sapevo cosa stavo facendo con le mie sequenze pigre, ma per scopi di debug ho inserito alcune chiamate print / prn / pr, avendo temporaneamente dimenticato cosa stavo stampando. Divertente, perché il mio PC è tutto bloccato?

cercando di programmare Clojure in modo imperativo.

C'è una certa tentazione di creare un sacco di refs o di atomscrivere codice che incasini costantemente con il loro stato. Questo può essere fatto, ma non è una buona misura. Potrebbe anche avere prestazioni scadenti e raramente beneficiare di più core.

cercando di programmare Clojure al 100% funzionalmente.

Un rovescio della medaglia: alcuni algoritmi vogliono davvero un po 'di stato mutevole. Evitare religiosamente lo stato mutevole a tutti i costi può comportare algoritmi lenti o scomodi. Ci vuole giudizio e un po 'di esperienza per prendere la decisione.

cercando di fare troppo in Java.

Poiché è così facile raggiungere Java, a volte si è tentati di usare Clojure come wrapper del linguaggio di scripting attorno a Java. Certamente avrai bisogno di fare esattamente questo quando usi la funzionalità della libreria Java, ma ha poco senso (ad esempio) mantenere strutture di dati in Java, o usare tipi di dati Java come raccolte per le quali ci sono buoni equivalenti in Clojure.


13

Molte cose già menzionate. Ne aggiungo solo un altro.

Clojure if tratta gli oggetti booleani Java sempre come veri anche se il suo valore è falso. Quindi, se hai una funzione java land che restituisce un valore booleano java, assicurati di non controllarlo direttamente (if java-bool "Yes" "No") ma piuttosto (if (boolean java-bool) "Yes" "No").

Sono stato bruciato da questo con la libreria clojure.contrib.sql che restituisce i campi booleani del database come oggetti booleani java.


8
Nota che (if java.lang.Boolean/FALSE (println "foo"))non stampa foo. (if (java.lang.Boolean. "false") (println "foo"))fa, però, mentre (if (boolean (java.lang.Boolean "false")) (println "foo"))non ... Abbastanza confuso davvero!
Michał Marczyk,

Sembra funzionare come previsto in Clojure 1.4.0: (assert (=: false (if Boolean / FALSE: true: false)))
Jakub Holý

Mi sono anche scottato di recente da questo quando ho fatto (filtro: mykey coll) dove: i valori di mykey dove Booleans - funziona come previsto con le raccolte create da Clojure, ma NON con le raccolte deserializzate, quando serializzato utilizzando la serializzazione Java predefinita, perché quei booleani sono deserializzati come nuovo Boolean () e purtroppo (nuovo Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon

1
Basta ricordare le regole di base di valori booleani in Clojure - nile falsesono false, e tutto il resto è vero. Un Java Booleannon nillo è e non lo è false(perché è un oggetto), quindi il comportamento è coerente.
erikprice

13

Tenere la testa in loop.
Rischi di esaurire la memoria se esegui il loop sugli elementi di una sequenza pigra potenzialmente molto grande o infinita mantenendo un riferimento al primo elemento.

Dimenticando che non c'è TCO.
Le normali chiamate di coda consumano spazio nello stack e se non stai attento, andranno in overflow. Clojure ha 'recure 'trampolinedeve gestire molti dei casi in cui i richiami della coda ottimizzati sarebbero usati in altre lingue, ma queste tecniche devono essere applicate intenzionalmente.

Sequenze non proprio pigre.
Puoi costruire una sequenza pigra con 'lazy-seqo 'lazy-cons(o costruendo su API pigre di livello superiore), ma se la includi 'veco la passi attraverso qualche altra funzione che realizza la sequenza, allora non sarà più pigra. Sia lo stack che l'heap possono essere sovrascritti da questo.

Mettere cose mutevoli in refs.
Tecnicamente puoi farlo, ma solo il riferimento all'oggetto nel ref stesso è governato dall'STM, non l'oggetto indicato e i suoi campi (a meno che non siano immutabili e puntino ad altri ref). Quindi, quando possibile, preferisci solo gli oggetti immutabili in refs. La stessa cosa vale per gli atomi.


4
l'imminente ramo di sviluppo fa molto per ridurre il primo elemento cancellando i riferimenti agli oggetti in una funzione una volta che diventano localmente irraggiungibili.
Arthur Ulfeldt

9

utilizzando loop ... recurper elaborare sequenze quando map farà.

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

vs.

(map do-stuff data)

La funzione map (nel ramo più recente) utilizza sequenze a blocchi e molte altre ottimizzazioni. Inoltre, poiché questa funzione viene eseguita frequentemente, l'Hotspot JIT di solito lo ha ottimizzato e pronto per l'uso senza alcun "tempo di riscaldamento".


1
Queste due versioni in realtà non sono equivalenti. La tua workfunzione è equivalente a (doseq [item data] (do-stuff item)). (Oltre al fatto, quel ciclo di lavoro non finisce mai.)
kotarak

sì, il primo rompe la pigrizia sui suoi argomenti. il seq risultante avrà gli stessi valori sebbene non sia più un seq pigro.
Arthur Ulfeldt

+1! Ho scritto numerose piccole funzioni ricorsive solo per scoprire un altro giorno in cui queste potrebbero essere generalizzate usando mape / o reduce.
nperson325681

5

I tipi di raccolta hanno comportamenti diversi per alcune operazioni:

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]

Lavorare con le stringhe può creare confusione (ancora non le capisco). Nello specifico, le stringhe non sono le stesse delle sequenze di caratteri, anche se le funzioni di sequenza funzionano su di esse:

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

Per estrarre una stringa, devi fare:

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

3

troppe parentesi, specialmente con la chiamata al metodo java void all'interno che si traduce in NPE:

public void foo() {}

((.foo))

si traduce in NPE da parentesi esterne perché le parentesi interne valutano zero.

public int bar() { return 5; }

((.bar)) 

risulta più facile da eseguire il debug:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.