2 pontos por GN⁺ 2024-09-06 | 1 comentários | Compartilhar no WhatsApp
  • 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-seq e delay usam locks em vez de synchronized, reduzindo situações em que I/O bloqueante prende uma thread real
  • No REPL, é possível adicionar bibliotecas com add-lib, add-libs e sync-deps sem 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, Supplier e funções para processar Java Stream
  • Em desempenho e compatibilidade, estão incluídos spliterator para PersistentVector, processamento mais eficiente de drop/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-seq e delay executavam código do usuário dentro de blocos synchronized para garantir comportamento de execução única
    • No JDK 21, synchronized ainda 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-seq e delay usam locks em vez de blocos synchronized
  • 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.alpha foi atualizado para 0.5.238
    • core.specs.alpha foi 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 vez
    • sync-deps: chama add-libs para libs que estão em deps.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/repl vincula esse sinalizador automaticamente
    • no REPL de clojure.main, as novas funções são automaticamente referidas no namespace user
    • em outros REPLs, pode ser necessário (require '[clojure.repl.deps :refer :all])
  • A resolução e o download de bibliotecas ficam a cargo do tools.deps
    • para evitar colocar tools.deps e 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
  • clojure.tools.deps.interop/invoke-tool chama 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-lib foi construído com invoke-tool e 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

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
    • start: permite controle total dos streams e acesso aos objetos Java subjacentes para usos avançados
    • exec: cobre o caso comum de executar um processo externo e retornar o stdout ao final

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
  • 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ático
    • Classname/.method: valor de função Clojure que chama um método de instância
    • Classname/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/method e Classname/.method
  • Os metadados :param-tags sã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-tags ao 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 @FunctionalInterface e 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
  • A interoperabilidade com Supplier também foi melhorada
    • antes, para chamar métodos que recebem um Supplier de valores, era preciso escrever um adaptador com reify
    • implementações de IDeref do Clojure, como delay, future e atom, agora implementam diretamente a interface Supplier

Processamento de Stream e melhorias de desempenho em coleções

  • Foram adicionadas funções para consumir no estilo Clojure os Stream que 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
  • PersistentVector agora 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 PersistentVector suporta paralelismo e melhora bastante o desempenho
  • A eficiência de drop, nthrest, nthnext e 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 range e repeat
    • as novas funções partitionv, partitionv-all e splitv-at são mais eficientes que suas equivalentes existentes e produzem partições em vetor em vez de partições em seq já realizadas

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"
  • Essa política trata a causa raiz de problemas que vieram à tona quando novas funções, especialmente abs, foram adicionadas a clojure.core no Clojure 1.11.0
    • se código compilado em versões anteriores do Clojure tiver um nome de var igual ao de uma função recém-adicionada em clojure.core, ele pode ficar unbound ao ser carregado em runtime 1.11.0
    • além de CLJ-2711, a correção anterior nessa área em CLJ-1604 foi revertida

Lista completa de mudanças

1 comentários

 
GN⁺ 2024-09-06
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.

    • Não sei se alguém ainda se lembra do Groovy. O Groovy tem uma anotação @Grab, que faz essencialmente a mesma coisa que o add-libs descrito, e é muito conveniente para escrever scripts.
    • Java agora também tem REPL e suporte a scripting, mas algo como add-libs ainda não está entre os metacomandos disponíveis.
  • 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] ...).

    • Ao ver “maybe not”, eu realmente precisei de schema/select e criei uma biblioteca para Malli: https://github.com/eval/malli-select
    • Fiquei curioso sobre o que significa “não dá para verificar macros”. Com spec, dá para colocar s/fdef em macros, e as chamadas são verificadas em tempo de compilação.
      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.

    • Rich Hickey e a Clojure Team são designers muito cuidadosos, então acho que não há motivo para se preocupar demais.
    • Pura especulação, mas talvez, por ser o primeiro lançamento depois que Rich Hickey saiu do nubank, ele tenha conseguido dedicar mais atenção a ele.
  • 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?

    • O spec continua existindo e sendo usado. Também houve bastante trabalho posterior, mas ele está parado enquanto se avalia o que fazer em várias questões.
  • Parece um lançamento bastante sólido, e fico feliz que o Clojure ainda esteja indo bem.

    • Fico me perguntando se ele realmente está indo bem. Estou avaliando usá-lo em um novo projeto e o considero junto com o Clara[0]. Mas ele não parece tão mainstream quanto antes, e o ecossistema dá a impressão de estar mais esparso do que antigamente.
      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.