Lançamento do Clojure 1.12.0
(clojure.org)- Clojure 1.12.0 mantém o bytecode Java 8, mas foi anunciado como o último lançamento baseado em Java 8 antes que versões futuras movam a compatibilidade mínima com Java e a base de bytecode para um Java LTS mais recente
- Em ambientes de threads virtuais do JDK 21,
lazy-seqedelayusam locks em vez desynchronized, reduzindo situações em que I/O bloqueante prende uma thread real - No REPL, é possível adicionar bibliotecas com
add-lib,add-libsesync-depssem reiniciar a JVM, mas esse recurso é limitado ao uso interativo durante o desenvolvimento - A interoperabilidade com Java foi ampliada com method values,
:param-tags, sintaxe de classes de array, conversão para interfaces funcionais,Suppliere funções para processar JavaStream - Em desempenho e compatibilidade, estão incluídos spliterator para
PersistentVector, processamento mais eficiente dedrop/particionamento, política de interning de Var mais rígida, correção do CVE-2024-22871 e organização dos identificadores de serialização Java
Compatibilidade com Java 8, segurança e organização da serialização
- Informações de download e uso do Clojure 1.12.0 podem ser encontradas na página Downloads
- A base em Java 8 foi mantida nesta versão
- O Clojure 1.12 gera bytecode Java 8, assim como o Clojure 1.10 e 1.11
- Versões futuras devem mover o bytecode e a compatibilidade mínima com Java para uma versão Java LTS mais recente
- O problema de fixação de threads virtuais no JDK 21 foi amenizado
- Antes da 1.12,
lazy-seqedelayexecutavam código do usuário dentro de blocossynchronizedpara garantir comportamento de execução única - No JDK 21,
synchronizedainda não participa de bloqueio cooperativo, então, se esse código fizer I/O bloqueante, pode prender uma thread real - Ao usar
-Djdk.tracePinnedThreads=full, o JDK 21 pode emitir avisos sobre essa situação - Na 1.12,
lazy-seqedelayusam locks em vez de blocossynchronized
- Antes da 1.12,
- A correção de segurança CVE-2024-22871 foi aplicada, e o aviso relacionado está em GHSA-vr64-r9qj-h27f
- O serialVersionUID de classes relacionadas à serialização Java passou a ser definido explicitamente
- Os tipos de dados do Clojure implementam a interface de serialização Java desde o Clojure 1.0
- A serialização Java funciona exigindo que, na desserialização, corresponda um identificador gerado com base no nome da classe, hierarquia de tipos e campos serializáveis
- O Clojure não garante consistência de serialização entre versões, mas aplicou mudanças para ter mais controle no futuro e evitar quebrar compatibilidade além do necessário
- As dependências também foram atualizadas
spec.alphafoi atualizado para 0.5.238core.specs.alphafoi atualizado para 0.4.74
Recursos para lidar com bibliotecas e ferramentas no REPL
- Durante o desenvolvimento, às vezes é preciso adicionar bibliotecas sem reiniciar a JVM
- avaliação experimental
- adição de dependências já conhecidas no projeto
- adição de bibliotecas para tarefas específicas
- O Clojure 1.12 fornece novas funções para adicionar bibliotecas sem perder o estado do REPL
add-lib: baixa uma lib ausente do classpath e a adiciona ao classloader- não atualiza libs que já estão no classpath
- se não houver coordenadas, pode inferir a versão Maven mais recente ou, quando possível inferir o nome do repositório git, usar a versão ou tag git mais recente
add-libs: resolve várias novas bibliotecas e versões de uma vezsync-deps: chamaadd-libspara libs que estão emdeps.edn, mas ainda não estão no classpath
- Essas funções são destinadas apenas ao uso do REPL durante o desenvolvimento
- a forma correta de construir e manter código de produção continua sendo o uso de
deps.edn - as três funções verificam se
*repl*está vinculado a true clojure.main/replvincula esse sinalizador automaticamente- no REPL de
clojure.main, as novas funções são automaticamente referidas no namespaceuser - em outros REPLs, pode ser necessário
(require '[clojure.repl.deps :refer :all])
- a forma correta de construir e manter código de produção continua sendo o uso de
- A resolução e o download de bibliotecas ficam a cargo do tools.deps
- para evitar colocar
tools.depse suas dependências no classpath do projeto durante o desenvolvimento, também foi adicionada uma nova API que chama funções via Clojure CLI em um processo separado
- para evitar colocar
clojure.tools.deps.interop/invoke-toolchama funções de ferramentas em um processo separado- o classpath da ferramenta é definido em
deps.edn - não é necessário adicionar dependências da ferramenta ao classpath do projeto
- o recurso
add-libfoi construído cominvoke-toole também pode ser usado para montar ou chamar ferramentas do usuário de forma interativa - o protocolo de execução de funções pode ser consultado na referência da CLI
- o classpath da ferramenta é definido em
API para execução de processos externos
- Além do namespace existente
clojure.java.shell, surgiu um caminho que aproveita as novas APIs do Java para informações de processo, controle e redirecionamento de I/O - O Clojure 1.12 adiciona o novo namespace
clojure.java.process- aproveita as novas APIs de processo do Java
- foi projetado para ser mais fácil de usar do que a abordagem anterior
- As principais funções são as seguintes
Expansão da interoperabilidade com Java
- Foram adicionados method values, permitindo usar métodos Java de forma mais direta em funções de ordem superior
- antes, para passar um método Java a algo como
map, era preciso encapsulá-lo manualmente em uma função - esse encapsulamento manual era verboso e podia exigir hints para diferenciar overloads, além de gerar reflexão ou boxing adicionais
- agora, qualified methods podem ser usados em posição de valor como funções normais, e o compilador gera automaticamente uma função de encapsulamento
- se um qualified method não puder ser resolvido por causa de overloads, o compilador gera uma chamada por reflexão
- o desenvolvedor pode especificar a assinatura desejada de um único método com metadados
:param-tags
- antes, para passar um método Java a algo como
- A sintaxe de qualified method explicita a classe e o método
Classname/method: valor de função Clojure que chama um método estáticoClassname/.method: valor de função Clojure que chama um método de instânciaClassname/new: valor de função Clojure que chama um construtor- para distinguir métodos estáticos de métodos de instância, é preciso usar a sintaxe
Classname/methodeClassname/.method
- Os metadados
:param-tagssão usados para resolver métodos sobrecarregados- um qualified method usado como valor fornece apenas a classe e o nome do método, então não é possível resolver overloads
:param-tagsé um vetor na forma[tag …], e cada tag corresponde a um parâmetro da assinatura desejada- é possível usar o placeholder
_para parâmetros de tipos não sobrecarregados - ao fornecer
:param-tags, o compilador deve conseguir resolver um único método em tempo de compilação - a nova sintaxe de reader metadata
^[tag …]adiciona metadados:param-tagsao símbolo de membro
- Foi adicionada a sintaxe de classes de array
- o Clojure já dava suporte a símbolos de nome de classe como valores de objeto Class e type hints, mas não oferecia sintaxe de classe de array além de strings
- agora é possível referenciar classes de array com símbolos na forma
ComponentClass/#dimensions - exemplos:
String/1,java.lang.String/1,long/2 - a classe de componente pode ser um nome de classe completo, uma classe importada ou um tipo primitivo
- a sintaxe de classe de array pode ser usada tanto como type hint quanto como valor
- A interoperabilidade com interfaces funcionais Java foi melhorada
- interfaces funcionais Java têm
@FunctionalInterfacee um único método - funções Clojure podem ser passadas a chamadas de métodos Java que recebem interfaces funcionais, desde que a aridade corresponda
- o compilador Clojure cria um adaptador lambda para converter implicitamente a função Clojure na interface funcional necessária
- para evitar a criação repetida de adaptadores em loops, é possível forçar isso explicitamente com hints em nomes vinculados por
let
- interfaces funcionais Java têm
- A interoperabilidade com
Suppliertambém foi melhorada- antes, para chamar métodos que recebem um
Supplierde valores, era preciso escrever um adaptador comreify - implementações de
IDerefdo Clojure, comodelay,futureeatom, agora implementam diretamente a interfaceSupplier
- antes, para chamar métodos que recebem um
Processamento de Stream e melhorias de desempenho em coleções
- Foram adicionadas funções para consumir no estilo Clojure os
Streamque as APIs Java vêm retornando cada vez mais- junto com o suporte a interfaces funcionais do Clojure 1.12, foram fornecidas funções de interoperabilidade com Stream
(stream-seq! stream) ⇒ seq(stream-reduce! f [init-val] stream) ⇒ val(stream-transduce! xf f [init-val] stream) ⇒ val(stream-into! to-coll [xf] stream) ⇒ to-coll- todas as funções são operações terminais de stream e consomem o stream
PersistentVectoragora fornece diretamente o spliterator usado pela implementação de stream de coleções Java- spliterator é um iterador divisível para iteração paralela mais rápida
- o novo spliterator customizado de
PersistentVectorsuporta paralelismo e melhora bastante o desempenho
- A eficiência de
drop,nthrest,nthnexte do processamento de partições foi melhorada- CLJ-2713 adiciona a interface interna
IDrop, indicando que uma coleção consegue fazer drop de forma mais eficiente do que por iteração sequencial - essa interface é implementada em coleções persistentes e em coleções algorítmicas como
rangeerepeat - as novas funções
partitionv,partitionv-allesplitv-atsão mais eficientes que suas equivalentes existentes e produzem partições em vetor em vez de partições em seq já realizadas
- CLJ-2713 adiciona a interface interna
Política de interning de Var mais rígida
- Fazer interning de uma var em um namespace, ao contrário de aliasing, significa criar uma referência estável para que todas as referências obtenham o mesmo objeto
- Antes, havia alguns casos em que uma var com interning podia ser substituída, e a política ficou mais rígida na 1.12.0-alpha1
- quando isso acontece, aparece um aviso no formato
"REJECTED: attempt to replace interned var #'some-ns/foo with #'other-ns/foo in some-ns, you must ns-unmap first"
- quando isso acontece, aparece um aviso no formato
- Essa política trata a causa raiz de problemas que vieram à tona quando novas funções, especialmente
abs, foram adicionadas aclojure.coreno Clojure 1.11.0
Lista completa de mudanças
- A lista completa de mudanças do Clojure 1.12.0 está no changelog oficial
1 comentários
Comentários do Hacker News
É realmente um grande lançamento e traz muitos recursos novos interessantes.
Pessoalmente, o que mais gostei foi o add-libs. Agora dá para criar demos em um único arquivo ou exemplos mínimos para reproduzir problemas, reduzindo muito a barreira para compartilhar pequenos trechos de código executáveis.
Também dá para demonstrar bibliotecas Java sem boilerplate de Java. Depois de experimentar coisas no REPL, você cola o código em um comentário no HN, por exemplo, e qualquer pessoa consegue reproduzir exatamente a mesma “configuração” e executá-lo. Nem precisa clonar um repositório.
Achei que eles fossem adiar este lançamento até a Clojure/conj 2024. Não que eu tivesse algum fundamento específico, mas o Clojure 1.10 saiu por volta da Clojure/conj 2021, e o anúncio da gratuidade do Datomic também aconteceu no início da Clojure/conj 2023.
Ainda assim, continuo esperando pelo spec2. Hoje contorno a rigidez do spec com o Malli, mas ele não é cidadão de primeira classe no Clojure. Principalmente porque não dá para verificar macros, e isso é intencional pelo design do compilador do Clojure. Dito isso, se você manipular schemas Malli como dados, dá para imitar a ideia de schema/select.
Graças à mudança nas interfaces funcionais, agora é possível passar funções diretamente, sem precisar manter macros utilitárias como
(defmacro ->Consumer [f] ...).Na prática, o próprio Clojure core também verifica chamadas de macros com spec, e às vezes dá para ver esses rastros no stack trace quando elas são chamadas incorretamente. Fiquei curioso se você quis dizer outra coisa.
É muito bom que o código existente continue funcionando mesmo com tantos recursos novos. O esforço consistente para evitar quebras de compatibilidade realmente se destaca.
Se quiser saber mais sobre Clojure, vale conferir a conferência Clojure/conj, que acontece de 23 a 25 de outubro em Alexandria, Virgínia: https://2024.clojure-conj.org
Fico feliz com a chegada de add-libs e sync-deps. Agora parece que há poucos motivos, ou talvez nenhum, para ter que encerrar uma sessão.
Este lançamento parece ter um escopo bem diferente dos anteriores, e é interessante ver quanta coisa entrou. Só espero que, por causa do ritmo mais rápido, isso não vire uma bola de coisas emaranhadas daqui a alguns lançamentos.
A mudança nas interfaces funcionais é enorme. Clojure fica melhor quando se mantém próximo do Java por meio de uma interoperabilidade cuidadosa, e esta mudança preenche uma grande lacuna.
Fico curioso sobre o que aconteceu com o spec. Foi abandonado? Há alguma notícia para esperar?
Parece um lançamento bastante sólido, e fico feliz que o Clojure ainda esteja indo bem.
Não estou tentando trollar. Quero escolhê-lo, e parece uma boa decisão de engenharia. Mas, se a popularidade e o número de contribuidores estiverem despencando, isso pode se tornar um obstáculo no futuro próximo.
[0] https://www.clara-rules.org/
Ficou mais fácil do que nunca transformar desenvolvedores existentes em desenvolvedores Clojure.
Um grande problema inicial[0] é ler o código, e IAs como ChatGPT ou Claude são muito boas em explicar código Clojure existente. Como resultado, o onboarding de desenvolvedores pode ficar muito mais rápido.
[0] Depois de algumas semanas, ler Clojure se torna natural, e você até esquece que antes não conseguia lê-lo.
Muitas melhorias bacanas. Normalmente é a principal linguagem da família Lisp à qual eu recorro.