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.
some
função do Clojure ou, melhor ainda, apenas usar acontains
si mesmo. Implementar coleções de Clojurejava.util.Collection
.(.contains [1 2 3] 2) => true
Respostas:
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:
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:
então eu acreditei que isso também funcionaria:
mas falha com:
porque a macro do leitor # () é expandida para
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
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:
enquanto Dan mostra que você pode usar a função de identidade para desembrulhar o parêntese externo:
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:
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:
Quando, na verdade, o loop teria sido mais conciso e idiomático para esta função específica:
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:
Outra maneira pela qual eles são diferentes é que podem operar em sequências infinitas preguiçosas:
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:
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:
quando, na verdade, Clojure suporta literais BigDecimal anexando M ao número:
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:
o que aumentou minhas referências de função totalmente qualificadas:
Para complicar ainda mais as coisas, usei uma estrutura de diretório padrão do Maven :
que é mais complexo do que a estrutura Clojure "padrão" de:
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:
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.
fonte
(#(hash-set %1 %2) :a 1)
ou neste caso(hash-set :a 1)
.do
:(#(do {%1 %2}) :a 1)
.hash-map
diretamente (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 usodo
em 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.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.
O
map
nunca é avaliado, é descartado silenciosamente, porque é preguiçoso. Tem de usar um dedoseq
,dorun
,doall
etc, para forçar a avaliação das sequências de descanso para os efeitos secundários.Usar um bare
map
no 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.fonte
(map ...)
por dentro(binding ...)
e me perguntando por que os novos valores de ligação não se aplicam.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
ref
s ou seatom
escrever 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.
fonte
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.
fonte
(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!nil
efalse
são falsas, e tudo o mais é verdadeiro. Um JavaBoolean
não énil
e não éfalse
(porque é um objeto), portanto, o comportamento é consistente.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
'recur
e'trampoline
para 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-seq
ou'lazy-cons
(ou construindo em APIs preguiçosas de nível superior), mas se você envolvê-la'vec
ou 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.
fonte
usando
loop ... recur
para processar sequências quando o mapa for suficiente.vs.
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".
fonte
work
função é equivalente a(doseq [item data] (do-stuff item))
. (Além do fato, esse ciclo de trabalho nunca termina.)map
e / oureduce
.Os tipos de coleção têm comportamentos diferentes para algumas operações:
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:
Para retirar uma corda, você precisa fazer:
fonte
muitos parênteses, especialmente com a chamada de método void java dentro do qual resulta em NPE:
resulta em NPE dos parênteses externos porque os parênteses internos são avaliados como nulo.
resulta em mais fácil de depurar:
fonte