Erros de programação comuns para desenvolvedores Clojure evitarem [fechado]

92

Quais são alguns erros comuns cometidos por desenvolvedores do Clojure e como podemos evitá-los?

Por exemplo; os recém-chegados ao Clojure pensam que a contains?função funciona da mesma forma que java.util.Collection#contains. No entanto, contains?só funcionará da mesma forma quando usado com coleções indexadas como mapas e conjuntos e você estiver procurando por uma determinada chave:

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

Quando usado com coleções indexadas numericamente (vetores, matrizes), verifica contains? apenas se o elemento fornecido está dentro do intervalo válido de índices (com base em zero):

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

Se for fornecida uma lista, contains?nunca retornará verdadeiro.

fogus
fonte
4
Apenas para sua informação, para aqueles desenvolvedores de Clojure procurando por java.util.Collection # contains funcionalidade type, uma olhada em clojure.contrib.seq-utils / includes? Dos documentos: Uso: (inclui? Coll x). Retorna verdadeiro se coll contém algo igual (com =) ax, em tempo linear.
Robert Campbell
11
Você parece ter perdido o fato de que essas perguntas são Community Wiki
3
Adoro como a questão Perl precisa estar fora de compasso com todas as outras :)
Éter
8
Para desenvolvedores de Clojure que procuram contém, eu recomendo não seguir o conselho de rcampbell. seq-utils há muito tempo está obsoleto e essa função nunca foi útil para começar. Você pode usar a somefunção do Clojure ou, melhor ainda, apenas usar a containssi mesmo. Implementar coleções de Clojure java.util.Collection. (.contains [1 2 3] 2) => true
Rayne de

Respostas:

70

Octais literais

A certa altura, eu estava lendo uma matriz que usava zeros à esquerda para manter linhas e colunas adequadas. Matematicamente, isso está correto, pois o zero à esquerda obviamente não altera o valor subjacente. As tentativas de definir uma var com esta matriz, no entanto, falhariam misteriosamente com:

java.lang.NumberFormatException: Invalid number: 08

o que me deixou totalmente perplexo. O motivo é que Clojure trata valores inteiros literais com zeros à esquerda como octais, e não há número 08 no octal.

Devo também mencionar que Clojure oferece suporte a valores hexadecimais Java tradicionais por meio do prefixo 0x . Você também pode usar qualquer base entre 2 e 36 usando a notação "base + r + valor", como 2r101010 ou 36r16 que são 42 base dez.


Tentando retornar literais em um literal de função anônima

Isso funciona:

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

então eu acreditei que isso também funcionaria:

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

mas falha com:

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

porque a macro do leitor # () é expandida para

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

com o literal do mapa entre parênteses. Como é o primeiro elemento, ele é tratado como uma função (o que um mapa literal realmente é), mas nenhum argumento obrigatório (como uma chave) é fornecido. Em resumo, o literal de função anônima não se expande para

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

e, portanto, você não pode ter nenhum valor literal ([],: a, 4,%) como o corpo da função anônima.

Duas soluções foram dadas nos comentários. Brian Carper sugere o uso de construtores de implementação de sequência (mapa de matriz, conjunto de hash, vetor) assim:

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

enquanto Dan mostra que você pode usar a função de identidade para desembrulhar o parêntese externo:

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

A sugestão de Brian realmente me leva ao meu próximo erro ...


Pensar que o hash-map ou array-map determina a implementação do mapa concreto imutável

Considere o seguinte:

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

Embora você geralmente não precise se preocupar com a implementação concreta de um mapa Clojure, você deve saber que as funções que desenvolvem um mapa - como assoc ou conj - podem pegar um PersistentArrayMap e retornar um PersistentHashMap , que tem um desempenho mais rápido para mapas maiores.


Usando uma função como ponto de recursão em vez de um loop para fornecer ligações iniciais

Quando comecei, escrevi muitas funções como esta:

; 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, na verdade, o loop teria sido mais conciso e idiomático para esta função específica:

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

Observe que substituí o argumento vazio, corpo da função "construtor padrão" (p3 775147 600851475143 3) por um loop + ligação inicial. A recorrência agora religa as ligações de loop (em vez dos parâmetros FN) e salta de volta para o ponto de recursão (circular, em vez de fn).


Referenciando vars "fantasmas"

Estou falando sobre o tipo de var que você pode definir usando o REPL - durante sua programação exploratória - e, sem saber, fazer referência em sua fonte. Tudo funciona bem até que você recarregue o namespace (talvez fechando seu editor) e depois descubra um monte de símbolos não acoplados referenciados em todo o seu código. Isso também acontece com frequência quando você está refatorando, movendo uma var de um namespace para outro.


Tratar a compreensão da lista for como um loop for imperativo

Essencialmente, você está criando uma lista preguiçosa com base em listas existentes, em vez de simplesmente executar um loop controlado. O doseq de Clojure é, na verdade, mais análogo aos construtos foreach imperativos de looping.

Um exemplo de como eles são diferentes é a capacidade de filtrar quais elementos eles iteram usando predicados arbitrários:

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)

Outra maneira pela qual eles são diferentes é que podem operar em sequências infinitas preguiçosas:

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

Eles também podem lidar com mais de uma expressão de vinculação, iterando sobre a expressão mais à direita primeiro e trabalhando para a esquerda:

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

Há também não quebrar ou continuar para sair prematuramente.


Uso excessivo de estruturas

Eu venho de uma formação OOPish, então quando comecei o Clojure meu cérebro ainda pensava em termos de objetos. Eu me descobri modelando tudo como uma estrutura porque seu agrupamento de "membros", por mais solto que fosse, me deixava confortável. Na realidade, as estruturas devem ser consideradas principalmente uma otimização; Clojure irá compartilhar as chaves e algumas informações de pesquisa para economizar memória. Você pode otimizá-los ainda mais definindo acessores para acelerar o processo de pesquisa de chave.

No geral, você não ganha nada com o uso de uma estrutura sobre um mapa, exceto em desempenho, então a complexidade adicionada pode não valer a pena.


Usando construtores BigDecimal não açúcares

Eu precisava de muitos BigDecimals e estava escrevendo um código feio como este:

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

quando, na verdade, Clojure suporta literais BigDecimal anexando M ao número:

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

Usar a versão com açúcar elimina muito o inchaço. Nos comentários, twils mencionou que você também pode usar as funções bigdec e bigint para ser mais explícito, mas permanecer conciso.


Usando as conversões de nomenclatura de pacote Java para namespaces

Na verdade, isso não é um erro em si, mas sim algo que vai contra a estrutura idiomática e a nomenclatura de um projeto Clojure típico. Meu primeiro projeto Clojure substancial tinha declarações de namespace - e estruturas de pasta correspondentes - como esta:

(ns com.14clouds.myapp.repository)

o que aumentou minhas referências de função totalmente qualificadas:

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

Para complicar ainda mais as coisas, usei uma estrutura de diretório padrão do Maven :

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

que é mais complexo do que a estrutura Clojure "padrão" de:

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

que é o padrão dos projetos Leiningen e do próprio Clojure .


Os mapas utilizam equals () do Java em vez de Clojure = para correspondência de chave

Relatado originalmente por chouser no IRC , esse uso de equals () do Java leva a alguns resultados não intuitivos:

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

Como as instâncias Integer e Long de 1 são impressas da mesma forma por padrão, pode ser difícil detectar por que seu mapa não está retornando nenhum valor. Isso é especialmente verdadeiro quando você passa sua chave por meio de uma função que, talvez sem seu conhecimento, retorna um long.

Deve-se observar que o uso de equals () do Java em vez de = de Clojure é essencial para que os mapas estejam em conformidade com a interface java.util.Map.


Estou usando Programming Clojure de Stuart Halloway, Practical Clojure de Luke VanderHart e a ajuda de incontáveis ​​hackers de Clojure no IRC e na lista de e-mails para ajudar nas minhas respostas.

campainha
fonte
1
Todas as macros do leitor têm uma versão de função normal. Você poderia fazer (#(hash-set %1 %2) :a 1)ou neste caso (hash-set :a 1).
Brian Carper
2
Você também pode 'remover' os parênteses adicionais com a identidade: (# (identidade {% 1% 2}): a 1)
1
Você também pode usar do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - Eu não gosto dessa solução, tanto quanto os anteriores, pois fazer implica que um efeito colateral está ocorrendo, quando na verdade este não é o caso aqui.
Robert Campbell
@ rrc7cz: Bem, na realidade, não há necessidade de usar uma função anônima aqui, já que usar hash-mapdiretamente (como em (hash-map :a 1)ou (map hash-map keys vals)) é mais legível e não implica que algo especial e ainda não implementado em uma função nomeada está ocorrendo (o que o uso de #(...)implica, eu acho). Na verdade, o uso excessivo de fns anônimos é uma pegadinha para se pensar. :-) OTOH, às vezes eu uso doem funções anônimas superconcisas que são livres de efeitos colaterais ... Tende a ser óbvio que eles estão à primeira vista. Uma questão de gosto, eu acho.
Michał Marczyk
42

Esquecendo de forçar a avaliação de seqs preguiçosos

Seqs preguiçosos não são avaliados, a menos que você peça para serem avaliados. Você pode esperar que isso imprima algo, mas não imprime.

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

O mapnunca é avaliado, é descartado silenciosamente, porque é preguiçoso. Tem de usar um de doseq, dorun, doalletc, para forçar a avaliação das sequências de descanso para os efeitos secundários.

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

Usar um bare mapno REPL meio que parece funcionar, mas só funciona porque o REPL força a avaliação dos próprios seqs lazy. Isso pode tornar o bug ainda mais difícil de perceber, porque seu código funciona no REPL e não funciona a partir de um arquivo fonte ou dentro de uma função.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Brian Carper
fonte
1
+1. Isso me incomodou, mas de uma maneira mais insidiosa: eu estava avaliando (map ...)por dentro (binding ...)e me perguntando por que os novos valores de ligação não se aplicam.
Alex B
20

Sou um novato do Clojure. Usuários mais avançados podem ter problemas mais interessantes.

tentando imprimir sequências preguiçosas infinitas.

Eu sabia o que estava fazendo com minhas sequências preguiçosas, mas para fins de depuração, inseri algumas chamadas print / prn / pr, tendo esquecido temporariamente o que estava imprimindo. Engraçado, por que meu PC está todo desligado?

tentando programar o Clojure imperativamente.

Há alguma tentação de criar um monte de refs ou se atomescrever código que constantemente mode com seu estado. Isso pode ser feito, mas não é um bom ajuste. Ele também pode ter um desempenho ruim e raramente se beneficiar de vários núcleos.

tentando programar Clojure 100% funcionalmente.

Um outro lado disso: alguns algoritmos realmente querem um pouco de estado mutável. Evitar religiosamente o estado mutável a todo custo pode resultar em algoritmos lentos ou desajeitados. É preciso julgamento e um pouco de experiência para tomar uma decisão.

tentando fazer muito em Java.

Por ser tão fácil chegar ao Java, às vezes é tentador usar o Clojure como um wrapper de linguagem de script em torno do Java. Certamente você precisará fazer exatamente isso ao usar a funcionalidade da biblioteca Java, mas não faz sentido (por exemplo) manter estruturas de dados em Java ou usar tipos de dados Java, como coleções para as quais existem bons equivalentes em Clojure.

Carl Smotricz
fonte
13

Muitas coisas já mencionadas. Vou apenas adicionar mais um.

Clojure if trata os objetos Booleanos Java sempre como verdadeiros, mesmo se seu valor for falso. Portanto, se você tiver uma função java land que retorna um valor java Boolean, certifique-se de não verificar diretamente, (if java-bool "Yes" "No") mas sim (if (boolean java-bool) "Yes" "No").

Fiquei queimado com isso com a biblioteca clojure.contrib.sql que retorna campos booleanos de banco de dados como objetos booleanos java.

Vagif Verdi
fonte
8
Observe que (if java.lang.Boolean/FALSE (println "foo"))não imprime foo. (if (java.lang.Boolean. "false") (println "foo"))no entanto, ao passo (if (boolean (java.lang.Boolean "false")) (println "foo"))que não ... Muito confuso mesmo!
Michał Marczyk
Parece funcionar conforme o esperado em Clojure 1.4.0: (assert (=: false (if Boolean / FALSE: true: false)))
Jakub Holý
Eu também fui queimado por este recentemente ao fazer (filter: mykey coll) where: mykey's values ​​onde Booleans - funciona como esperado com coleções criadas por Clojure, mas NÃO com coleções desserializadas, quando serializadas usando serialização Java padrão - porque esses Booleans são desserializados como new Boolean () e, infelizmente (new Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon
1
Lembre-se apenas das regras básicas dos valores booleanos em Clojure - nile falsesão falsas, e tudo o mais é verdadeiro. Um Java Booleannão é nile não é false(porque é um objeto), portanto, o comportamento é consistente.
erikprice
13

Mantendo sua cabeça em loops.
Você corre o risco de ficar sem memória se fizer um loop sobre os elementos de uma sequência preguiçosa potencialmente muito grande ou infinita, enquanto mantém uma referência ao primeiro elemento.

Esquecendo que não há TCO.
Chamadas finais regulares consomem espaço de pilha e irão transbordar se você não tomar cuidado. O Clojure tem 'recure 'trampolinepara lidar com muitos dos casos em que chamadas de cauda otimizadas seriam usadas em outras linguagens, mas essas técnicas têm que ser aplicadas intencionalmente.

Sequências não muito preguiçosas.
Você pode construir uma sequência preguiçosa com 'lazy-seqou 'lazy-cons(ou construindo em APIs preguiçosas de nível superior), mas se você envolvê-la 'vecou passá-la por alguma outra função que realize a sequência, ela não será mais preguiçosa. Tanto a pilha quanto o heap podem ser sobrecarregados por isso.

Colocando coisas mutáveis ​​nos refs.
Você pode fazer isso tecnicamente, mas apenas a referência do objeto no próprio ref é regida pelo STM - não o objeto referido e seus campos (a menos que sejam imutáveis ​​e apontem para outros refs). Portanto, sempre que possível, prefira apenas objetos imutáveis ​​nos refs. A mesma coisa vale para átomos.

Colete chris
fonte
4
a próxima ramificação de desenvolvimento percorre um longo caminho para reduzir o primeiro item, apagando referências a objetos em uma função, uma vez que eles se tornam localmente inacessíveis.
Arthur Ulfeldt
9

usando loop ... recurpara processar sequências quando o mapa for suficiente.

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

vs.

(map do-stuff data)

A função de mapa (no ramo mais recente) usa sequências em partes e muitas outras otimizações. Além disso, como essa função é executada com frequência, o Hotspot JIT geralmente a tem otimizada e pronta para funcionar sem qualquer "tempo de aquecimento".

Arthur Ulfeldt
fonte
1
Na verdade, essas duas versões não são equivalentes. Sua workfunção é equivalente a (doseq [item data] (do-stuff item)). (Além do fato, esse ciclo de trabalho nunca termina.)
kotarak
sim, o primeiro quebra a preguiça de seus argumentos. o seq resultante terá os mesmos valores, embora não seja mais um seq preguiçoso.
Arthur Ulfeldt
+1! Eu escrevi várias pequenas funções recursivas apenas para descobrir outro dia em que todas elas poderiam ser generalizadas usando mape / ou reduce.
nperson325681
5

Os tipos de coleção têm comportamentos diferentes para algumas operações:

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]

Trabalhar com strings pode ser confuso (ainda não entendi direito). Especificamente, as strings não são iguais às sequências de caracteres, embora as funções de sequência funcionem nelas:

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

Para retirar uma corda, você precisa fazer:

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

muitos parênteses, especialmente com a chamada de método void java dentro do qual resulta em NPE:

public void foo() {}

((.foo))

resulta em NPE dos parênteses externos porque os parênteses internos são avaliados como nulo.

public int bar() { return 5; }

((.bar)) 

resulta em mais fácil de depurar:

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