7 pontos por GN⁺ 2025-05-18 | 4 comentários | Compartilhar no WhatsApp
  • A proposta de Explicit Resource Management introduz uma nova forma de controlar com clareza o ciclo de vida de recursos como identificadores de arquivo, conexões de rede e outros
  • O recurso está disponível a partir do Chromium 134 e do V8 v13.8
  • O que é adicionado à linguagem
    • As declarações using e await using, junto com os símbolos Symbol.dispose e Symbol.asyncDispose, fornecem um mecanismo de limpeza automática
    • DisposableStack e AsyncDisposableStack agrupam e liberam vários recursos com segurança
    • SuppressedError gerencia em conjunto os erros ocorridos durante a limpeza e o erro original
  • Essa abordagem aumenta bastante a segurança e a manutenibilidade do código, além de ser eficaz para evitar vazamentos de recursos
  • Ela simplifica o padrão tradicional try...finally e permite um tratamento de recursos confiável em ambientes grandes e complexos com múltiplos recursos

Visão geral da proposta de gerenciamento explícito de recursos

  • A proposta de Explicit Resource Management introduz uma nova forma de criar e liberar com clareza recursos como identificadores de arquivo e conexões de rede
  • Os principais componentes são os seguintes
    • Declarações using e await using: liberam recursos automaticamente ao sair do escopo
    • Símbolos [Symbol.dispose]() e [Symbol.asyncDispose]() : métodos para implementar a ação de liberação (cleanup)
    • Objetos globais DisposableStack e AsyncDisposableStack: agrupam vários recursos para gerenciamento eficiente
    • SuppressedError: novo tipo de erro que inclui tanto o erro ocorrido durante a limpeza quanto o erro original
  • Esses recursos têm como foco permitir que desenvolvedores façam um gerenciamento detalhado de recursos e melhorem o desempenho e a segurança do código

Declarações using e await using

  • A declaração using é usada para recursos síncronos, e await using para recursos assíncronos
  • Os recursos declarados chamam automaticamente Symbol.dispose ou Symbol.asyncDispose ao saírem do escopo
  • Com isso, é possível reduzir problemas de vazamento em recursos síncronos e assíncronos e escrever código de liberação consistente
  • Essas palavras-chave só podem ser usadas dentro de blocos de código, loops for e corpos de função, e não podem ser usadas no nível superior
  • Exemplo
    • Por exemplo, ao usar ReadableStreamDefaultReader, é necessário chamar reader.releaseLock() para que o stream possa ser reutilizado
    • Se ocorrer um erro e essa chamada for omitida, pode surgir o problema de o stream ficar bloqueado permanentemente
  • Forma tradicional
    • Desenvolvedores usam um bloco try...finally para garantir a liberação do bloqueio do leitor
    • É necessário escrever reader.releaseLock() no bloco finally
  • Forma aprimorada: introdução de using
    • Cria-se um objeto descartável (readerResource) que inclui a ação de liberação
    • Ao usar o padrão using readerResource = {...}, a liberação acontece automaticamente assim que o bloco é deixado
    • No futuro, se as Web APIs passarem a oferecer suporte a [Symbol.dispose] e [Symbol.asyncDispose], poderá ser possível fazer esse gerenciamento automaticamente sem escrever objetos wrapper separados

DisposableStack e AsyncDisposableStack

  • DisposableStack e AsyncDisposableStack são introduzidos para agrupar vários recursos de forma eficiente e segura
  • Ao adicionar recursos a cada stack e liberar o próprio stack, todos os recursos internos são liberados em ordem inversa
  • Isso reduz riscos e simplifica o código ao lidar com conjuntos complexos de recursos que têm dependências entre si
  • Principais métodos
    • use(value): adiciona um recurso descartável ao topo da stack
    • adopt(value, onDispose): adiciona um recurso não descartável junto com um callback de liberação
    • defer(onDispose): adiciona apenas a ação de liberação, sem um recurso
    • move(): move todos os recursos da stack atual para uma nova stack, permitindo transferir a propriedade
    • dispose(), asyncDispose(): liberam todos os recursos contidos na stack

Status de suporte e quando já pode ser usado

  • O recurso de gerenciamento explícito de recursos está disponível no Chromium 134 e no V8 v13.8 ou superiores
  • Há expectativa de expansão futura da compatibilidade com diversas Web APIs

4 comentários

 
cichol 2025-05-18

await using data = await fn()
o milagre de ter await tanto no lado esquerdo quanto no lado direito

 
GN⁺ 2025-05-18
Comentários no Hacker News
  • Esta proposta passa uma sensação parecida com o problema das "cores das funções". A distinção entre funções síncronas e assíncronas continua invadindo todos os recursos. Por exemplo, dá para ver isso nos casos de Symbol.dispose e Symbol.asyncDispose, DisposableStack e AsyncDisposableStack. Fico satisfeito que o Java tenha seguido para virtual threads. Acho que foi uma escolha de adicionar complexidade à JVM para reduzir a carga sobre desenvolvedores de aplicações, autores de bibliotecas e ferramentas de depuração

    • Não concordo com esconder a assincronicidade, porque isso tornaria mais difícil entender o fluxo do código. Eu também quero saber se um recurso é liberado de forma assíncrona e se isso pode ser afetado por fatores externos, como problemas de rede

    • Hoje em dia me irrita demais esse fenômeno de, na maioria das linguagens, "o normal é escrever todo o código como assíncrono". Vejo o Purescript como o único caso em que você escreve com Eff (efeitos síncronos) ou Aff (efeitos assíncronos) e pode escolher no ponto da chamada. Structured concurrency é legal, mas na prática parece menos um trabalho sintático para obter concorrência estruturada e mais um trabalho para ter vários handlers de requisições de topo em um servidor. Não passa de um meio para facilitar processamento paralelo

    • Não sei como isso foi implementado na JVM, mas, de modo geral, multithreading é uma tecnologia realmente pouco intuitiva de lidar. Há inúmeros livros sobre race conditions, deadlocks, livelocks, starvation, problemas de visibilidade de memória e assim por diante. Comparado a isso, programação assíncrona em thread única é bem menos pesada. Aceitar o problema das cores das funções parece uma escolha menos dolorosa do que depurar "Heisenbugs" em apps multithread

    • Fico realmente feliz que o Java tenha tomado essa decisão

    • A explicação é que execução normal e funções assíncronas formam closed Cartesian categories entre si. A categoria de execução normal pode ser embutida diretamente na categoria assíncrona. Toda função tem uma categoria, isto é, uma cor de função, e algumas linguagens deixam isso mais explícito. Isso é uma escolha de design da linguagem, e teoria das categorias pode ser aplicada com força muito além de threading. O Java e a abordagem baseada em threads acabam enfrentando problemas de sincronização, e isso tem a característica de ser especialmente difícil. O JavaScript restringe as categorias monádicas, em especial no estilo continuation-passing

  • Quando vi o exemplo de uso de using com uma função defer, achei muito inovador. Talvez para muita gente isso já seja intuitivo, mas ainda assim acho que vale mencionar

    • O uso de DisposableStack e AsyncDisposableStack, incluídos na proposta de using, dá suporte embutido para registrar callbacks. Como using tem escopo de bloco, isso é necessário para atravessar escopos ou fazer registro condicional. Mas variáveis using, como const, precisam ser inicializadas imediatamente, então não dá para fazer inicialização condicional. Nesses casos, é preciso o padrão de criar uma Stack no topo da função e colocar nela, com defer, os recursos usados. Se necessário, também é fácil ajustar o momento da liberação para o nível da função

    • Lembra bastante golang

  • Acho uma ideia muito boa, mas, mesmo que no futuro seja possível unificar [Symbol.dispose] e [Symbol.asyncDispose] em coisas como streams de Web API, no futuro próximo só parte das APIs e bibliotecas vai oferecer suporte ao recurso, enquanto o restante, provavelmente a maioria, não vai. No fim, surge o dilema entre misturar using com try/catch ou simplesmente usar try/catch em todo o código e escolher algo mais fácil de entender. Há o risco de isso render ao recurso a fama de "não dá para usar na prática". É uma pena, porque é um bom design para resolver um problema real, mas a adoção pode ser difícil

    • Em APIs que não suportam esse recurso, dá para aplicar using com DisposableStack. A vantagem é que, ao lidar com vários recursos ao mesmo tempo, isso fica muito mais simples do que try/catch. Se o runtime der suporte, já dá para usar imediatamente sem esperar atualização dos recursos existentes

    • No mundo JavaScript isso se repete há 15 anos. Recursos novos da linguagem primeiro entram em compiladores como Babel, depois na especificação, e por fim muitas vezes levam de 3 a 4 anos até aparecer em APIs estáveis e navegadores. Os desenvolvedores já estão acostumados a encapsular Web APIs com pequenos wrappers, e muitas vezes wrappers são melhores que polyfills. Nunca pensei "isso vai ser difícil de usar" só porque surgiu um novo recurso útil na linguagem

    • Na prática, muitos recursos já foram implementados como polyfill, então grande parte do ecossistema NodeJS usa esse padrão, e os usuários podem simplesmente ajustar a sintaxe com um transpiler. Quando preparei uma apresentação sobre isso no ano passado, descobri que NodeJS e bibliotecas importantes já tinham várias APIs com suporte a Symbol.dispose. No frontend isso talvez seja menos usado por causa dos sistemas de gerenciamento de ciclo de vida, mas em algumas situações ainda é útil. Acho que vai se espalhar bastante em bibliotecas de teste e no backend

    • O TC39 também precisa se concentrar em recursos fundamentais da linguagem, como traits/protocols ao estilo do Rust. No Rust é relativamente fácil definir e implementar um novo trait, enquanto no JS, por ser uma linguagem dinâmica e ter símbolos únicos, isso poderia ser introduzido de forma ainda mais simples. Há desvantagens, como a orphan rule, mas isso poderia evoluir para uma estrutura muito mais flexível

    • No mundo JavaScript normalmente isso se resolve com polyfills

  • Isso me lembra C#. Com IDisposable e IAsyncDisposable, é muito útil para abstrações como gerenciamento de locks, filas e escopos temporários

    • Como o autor da proposta veio da Microsoft, a sintaxe acabou ficando parecida com a de C#. Há um contexto consistente também nas issues relacionadas no GitHub

    • É basicamente um design emprestado do C#. A proposta original também faz referência ao context manager do Python, ao try-with-resources do Java e ao using statement do C#. A palavra-chave using e os métodos de hook de dispose já dão pistas bem fortes

  • Entendo que manter compatibilidade retroativa é importante no JavaScript, mas a sintaxe [Symbol.dispose]() parece estranha. Dá a impressão de um array com handles de método. Fiquei curioso sobre o que exatamente é essa sintaxe e quero entender melhor

    • A explicação é que chaves dinâmicas, envolvidas por colchetes no lado esquerdo de um objeto literal, já são usadas há quase 10 anos desde o ES6. Além disso, como símbolos não podem ser referenciados por string, combina-se chave dinâmica com sintaxe abreviada de método. No fundo, não é uma sintaxe nova

    • Com boas referências, a explicação é que isso vem da forma de atribuir chaves do tipo símbolo a objetos existentes. É uma evolução natural

    • Outras pessoas já explicaram o que é, mas talvez não tenham explicado o porquê. Usar um Symbol como nome de método garante que isso seja uma nova API sem conflito com métodos existentes. Também evita que uma classe seja tratada por engano como disposable

    • Menciona-se o conceito de dynamic property access. Propriedades de objeto podem ser acessadas com ponto (.) ou colchetes ([]), e suportam tanto strings quanto símbolos. Símbolos são comparados como objetos únicos, e well known symbols, como Symbol.dispose, garantem extensibilidade. Também foi explicada a semelhança com métodos __dunder__ do Python

    • Essa sintaxe já é usada há vários anos. O iterator do JavaScript usa o mesmo esquema e isso foi introduzido há quase 10 anos

  • Apresentação do motivo de ter havido esforço para introduzir concorrência estruturada em JS, especialmente quando o gerenciamento de recursos com escopo léxico é uma característica importante. Também foi compartilhada uma biblioteca relacionada de concorrência estruturada

  • Já há suporte ao recurso no Bun a partir da versão 1.0.23. Dá para experimentar de forma prática

  • Não consigo entender de jeito nenhum como alguém pode compreender e controlar o fluxo de execução de um programa com um estilo de código tão complexo

    • Esse é exatamente o ponto. 90% do desenvolvimento web são upgrades inúteis ou que ninguém quer, e depois a realidade é gastar os outros 10% do tempo consertando os problemas que eles criaram. Existe uma pequena chance de alguém precisar olhar um código escrito anos atrás, e aí a ideia é deixar os bugs como tarefa de entrada para um iniciante. Até sistemas legados de 20 anos continuam em uso

    • O código apresentado como exemplo tem muitos erros graves de sintaxe e está bem distante de JavaScript real. E desenvolvedores JS normalmente não misturam as coisas desse jeito, como while, promise chain e finally; o normal é usar await ou uma estrutura apropriada de tratamento de exceções. Em bibliotecas bem projetadas, também não se empilha camadas e mais camadas de handlers, e é possível escrever de forma bem mais concisa com DisposableStack. Hoje em dia, muitas vezes nem funções async imediatamente invocadas são mais necessárias

    • Se você trabalha profissionalmente com a linguagem e se acostuma com o significado e o comportamento das palavras-chave, acaba entendendo o código naturalmente. Programadores Haskell se acostumam da mesma forma

    • Ao incorporar código no HN, é preciso indentar cada linha com pelo menos 2 espaços. (Concordo que o código é difícil de entender)

    • A dica concisa de que indentação ajuda

  • Fiquei curioso sobre por que não seguiram com destrutores de classe anônima, ou com alguma estrutura que não usasse Symbol. Se existem dois Symbol (síncrono/assíncrono), isso levanta a questão de abstrações vazarem

    • Destrutores exigem comportamento previsível, em que o cleanup seja claro, e coletores de lixo avançados não combinam com esse padrão. Linguagens modernas dão suporte a cleanup baseado em escopo e implementam isso de várias formas, como HoF, hooks especiais e registro de callbacks. O Python no início se baseava em destrutores com GC por contagem de referência, mas por limitações acabou introduzindo context managers

    • Em outras linguagens, destrutores rodam conforme o timing do GC, então não são confiáveis. Já métodos dispose são chamados de forma clara quando o escopo da variável termina, então são previsíveis para coisas como fechar arquivos ou liberar locks. Métodos baseados em Symbol evitam conflitos com recursos existentes e, em geral, basta o autor da biblioteca se preocupar com isso. A distinção entre síncrono e assíncrono precisa ser explícita, e pode exigir uma sintaxe um pouco estranha, como await using a = await b()

    • Em linguagens com GC, destrutores costumam ser majoritariamente não determinísticos porque é difícil fazer chamadas síncronas. No JS existem WeakRef e FinalizationRegistry, mas nem a Mozilla recomenda o uso por serem imprevisíveis

    • Um ponto forte dessa abordagem é que ela pode ser usada também em alvos que não são instâncias de classe

    • No JavaScript não existe o conceito de propriedade anônima, então a própria pergunta parece ambígua. A alegação é que não haveria alternativa além desse método

  • O primeiro exemplo da proposta mostra um caso de código que libera um lock com segurança usando try/finally. Fiquei em dúvida se esse padrão só importa em situações de longa execução, ou se, em navegador ou ambiente CLI, o lock também seria liberado quando o processo terminasse por erro

    • A especificação diz que, quer a execução do bloco termine normalmente, quer termine por exceção, desvio ou saída, o dispose sempre será executado. Ou seja, é o mesmo tanto com using quanto com try/finally. Encerramento forçado, como matar o processo à força, fica fora do escopo da especificação, então o ECMAScript não trata disso. O stream do exemplo é um objeto interno do JS, então, se o interpretador desaparecer, a própria ideia de lock deixa de fazer sentido. Se forem recursos do sistema operacional, como memória e arquivos, normalmente o SO faz uma limpeza geral, mas o comportamento varia conforme a plataforma

    • Uma página web no navegador, por outro lado, é de certa forma uma aplicação de longa execução. Em alguns casos ela roda até mais do que um processo de servidor. Quando ocorre um erro, a página não morre por isso, e o tratamento de erro, inclusive de exceções, segue regras claras e passa pelo finally. No NodeJS, por padrão, o processo encerra em caso de erro, mas em ambientes de servidor é comum haver tratamento diferente. Ou seja, a função de liberação no finally certamente será chamada

 
ahwjdekf 2025-05-18

Até hoje a gente viveu muito bem sem dar a mínima para esse negócio de gerenciar recursos. Por que essa mudança repentina?