3 pontos por GN⁺ 2025-05-25 | 1 comentários | Compartilhar no WhatsApp
  • Efeitos algébricos (effect handlers) são uma ferramenta flexível de controle de fluxo que permite implementar em nível de biblioteca vários recursos de linguagem (tratamento de exceções, generators, corrotinas etc.)
  • Também podem ser aplicados a gerenciamento de contexto, injeção de dependência, substituição de estado global etc., comuns na programação funcional
  • Contribuem para a simplicidade do design de APIs e para a automatização da passagem de estado/ambiente dentro do código
  • Também oferecem vantagens como garantia de pureza funcional, replayability e auditoria de segurança
  • Com os avanços recentes nas tecnologias de compiladores, os problemas de desempenho também melhoraram bastante

Visão geral dos efeitos algébricos (Algebraic Effects)

Efeitos algébricos (também chamados de effect handlers) são um recurso de linguagem de programação que vem ganhando destaque recentemente. Eles estão se expandindo rapidamente como um dos recursos centrais do Ante e de várias linguagens de pesquisa (Koka, Effekt, Eff, Flix etc.). Muitos materiais explicam o conceito de effect handlers, mas ainda faltam explicações mais profundas sobre o "porquê" de eles serem necessários na prática. Este texto apresenta, da forma mais ampla possível, os usos reais e os benefícios dos efeitos algébricos.

Entendendo rapidamente a sintaxe e a semântica

  • Efeitos algébricos são um conceito semelhante a "exceções retomáveis"
  • É possível declarar funções de efeito como effect SayMessage
  • Como em foo () can SayMessage = ..., é possível indicar que a função pode usar esse efeito
  • Com handle foo () | say_message () -> ..., é possível tratá-lo como em um try/catch de exceções

Essa estrutura básica permite invocar e controlar efeitos.

Expansão de fluxo de controle definido pelo usuário

A principal razão para usar efeitos algébricos é que, com um único recurso de linguagem, dá para implementar como biblioteca funcionalidades que antes exigiam recursos separados da linguagem, como generators, exceções, corrotinas e assincronia.

  • Ao colocar variáveis de efeito polimórficas em funções (can e), é possível passar e combinar diferentes efeitos como argumentos
  • Por exemplo, a função map pode ser declarada de modo que a função recebida como argumento possa usar um efeito arbitrário e, permitindo combinação natural com vários efeitos (saída, assincronia etc.)

Exemplo de implementação de exceções e generators

  • Implementação de exceções: se o efeito for tratado sem chamar resume, ele funciona da mesma forma que uma exceção
  • Implementação de generators: ao definir o efeito Yield, o handler externo pode intervir toda vez que um valor é yielded e controlar o fluxo conforme a condição, permitindo até padrões mais avançados como filtragem com código relativamente simples

O fato de poder combinar vários efeitos também é uma grande vantagem em relação às técnicas tradicionais de abstração de efeitos.

Uso como camada de abstração

Efeitos algébricos não servem apenas para expandir recursos centrais de programação; eles também têm alta utilidade em vários cenários de aplicações de negócio.

Injeção de dependência (Dependency Injection)

  • Dependências como banco de dados e saída podem ser abstraídas como efeitos e gerenciadas por handlers
  • Também é possível implementar com flexibilidade a substituição por mocks de teste, redirecionamento de saída etc.

Logging condicional ou controle de saída

  • É possível controlar centralmente, de acordo com o nível de logging, se mensagens de log serão emitidas ou não

Simplificação do design de APIs e automatização da passagem de contexto

Uso de efeitos de estado (State)

  • Em situações em que é necessário passar um objeto de contexto ou informações de ambiente, se a implementação baseada em efeitos usar apenas get/set, torna-se possível automatizar o gerenciamento de estado sem passagem explícita
  • Antes era necessário passar o contexto como argumento para todas as funções, mas os state effects permitem ocultar essa parte

Substituição de objetos globais

  • Estados antes gerenciados como objetos globais, como geradores de números aleatórios e alocação de memória, também podem ser abstraídos como effects, o que favorece a clareza do código, a facilidade de teste e o suporte à concorrência
  • Basta trocar o handler para mudar com flexibilidade a fonte real de números aleatórios

Suporte à escrita em estilo direto (Direct Style)

  • Antes era preciso lidar com vários objetos aninhados usando option types, wrappers de erro etc.
  • Efeitos permitem expressar de forma limpa caminhos de erro ou efeitos colaterais sem esse empacotamento

Garantia de pureza e auditoria de segurança

Explicitação de efeitos colaterais

  • Na maioria das linguagens com effect handlers, funções que geram efeitos colaterais devem declarar efeitos como can IO, can Print etc. na assinatura de tipo
  • Em criação de threads, memória transacional de software (STM) etc., funções puras são obrigatórias

Replay de logs e networking determinístico

  • Com base na pureza, é possível criar handlers como record e replay para reproduzir resultados de execução
  • Isso permite oferecer resultados determinísticos e suporte a rollback em debugging, bancos de dados, redes de jogos etc.

Suporte a segurança baseada em capabilities

  • Todos os efeitos não tratados ficam expostos na assinatura de tipo da função, o que é eficaz para auditoria de segurança de bibliotecas externas
  • Se uma função antes sem efeitos colaterais for atualizada e passar a ter can IO, isso pode ser detectado imediatamente no código que a chama

No entanto, como todos os efeitos são propagados automaticamente, também pode haver o efeito colateral de eles serem tratados sem querer.

Perspectiva de eficiência e conclusão

  • Antes, problemas de eficiência de execução eram um ponto fraco, mas recentemente houve grande avanço em otimizações em muitos casos, como efeitos tail-resumptive
  • Diferentes linguagens vêm aplicando estratégias de compilação eficazes para esse modelo (closure call, evidence passing, especialização de handlers etc.)

Espera-se que os efeitos algébricos ocupem uma posição muito mais central nas linguagens de programação do futuro.

1 comentários

 
GN⁺ 2025-05-25
Opiniões no Hacker News
  • Vejo dois pontos negativos.
    O primeiro é que, ao olhar um trecho de código, não há qualquer indicação de que foo ou bar podem falhar.
    Para saber que essas chamadas podem acionar um tratador de erro, é preciso ir atrás da assinatura de tipos e, dependendo da situação, fazer isso manualmente sem ajuda da IDE.
    O segundo é que, depois de descobrir que foo e bar podem falhar, para encontrar qual código realmente roda quando isso acontece é preciso subir bastante pela call stack até achar uma expressão with, e depois descer acompanhando o tratador correspondente.
    Não dá para seguir esse comportamento estaticamente nem pular direto para a definição na IDE, porque my_function pode ser chamada em vários lugares com tratadores diferentes.
    Acho esse conceito muito interessante, mas no fim fico com receio em relação à legibilidade do código e à depuração.

    • Sobre o problema de descobrir qual código é executado quando a execução falha, isso seria justamente a essência da injeção dinâmica de código.
      Assim como em vários outros recursos dinâmicos, como shallow-binding e deep-binding, a vinculação acontece acompanhando a call stack.
      O fato de análise estática ou salto pela IDE não serem possíveis também decorre dessa natureza dinâmica.
      Mas acho que, na prática, isso não é algo com que seja tão necessário se preocupar.
      Como a ideia é adicionar efeitos a código puro, eles podem ser conectados em contextos diferentes conforme a situação, seja com efeitos puros ou impuros, seja com mocks para teste ou com o ambiente de produção.
      É um princípio parecido com injeção de dependência.
      Em mônadas tradicionais, algo parecido também pode ser implementado, mas para descobrir onde exatamente uma mônada foi instanciada também é preciso olhar a call stack.
      Essas técnicas trazem benefícios, mas também têm um custo claro.
      Elas ajudam em testes e sandboxing, mas deixam menos explícito no código o que está acontecendo.

    • Já escrevi uma monografia de graduação sobre suporte de IDE para efeitos e tratadores lexicais.
      Acho que todos os pontos levantados acima são perfeitamente factíveis.
      Link para a tese

    • No ecossistema .NET há uma tendência de usar interfaces em excesso, e isso já traz o incômodo de precisar passar por várias etapas para chegar à implementação de um método.
      Muitas vezes, se a implementação está em outro assembly, os recursos da IDE praticamente deixam de ajudar.
      Em Dependency Injection avançada, especialmente com Autofac, também se constrói uma hierarquia de escopos, como nas variáveis de escopo dinâmico do LISP, e em tempo de execução se decide a qual instância um serviço será vinculado.
      Nesse sentido, dá para injetar um efeito como uma instância de interface do tipo ISomeEffectHandler e representar a ocorrência do efeito como uma chamada de método nessa interface.
      O comportamento concreto do tratador, como lançar exceção ou registrar logs, passaria a ser decidido dinamicamente pela configuração de DI.
      Antes eu usava o padrão de dar throw em exceções, mas seria possível migrar para um design em que os efeitos são explicitados por interfaces e o tratamento fica inteiramente a cargo da DI.
      Não cheguei a investigar a fundo a parte de iteradores como yield.

    • Acho que o ponto central é justamente não haver indicação de que foo e bar podem falhar.
      Isso permite escrever código em estilo direto sem se preocupar com o contexto efetivo.
      Descobrir qual código vai rodar em caso de falha também faz parte da essência da abstração.
      Só mais tarde, no momento da execução, será decidido qual tratador de efeito concreto será acoplado.
      É o mesmo princípio de algo como f : g:(A -> B) -> t(A) -> B, em que não dá para saber antecipadamente qual código vai executar quando g for chamado.

    • Não concordo com a afirmação de que é impossível fazer análise estática subindo a call stack para encontrar o tratador.
      Na prática isso é possível, e uma IDE poderia oferecer algo como “ir para o chamador” para escolher quais tratadores estão sendo usados.

  • O “pseudocódigo” de Ante me impressionou bastante.
    Parece uma combinação muito feliz entre características de Haskell e a expressividade e praticidade de Elixir.
    Passa a sensação de ser um Haskell para desenvolvedores.
    Espero que o compilador amadureça.
    Tenho vontade de desenvolver um app em Ante.

  • Sobre a afirmação de que AE (Algebraic Effects) generaliza fluxo de controle a ponto de também permitir implementar corrotinas:
    acho que a forma mais simples de implementar AE em um runtime de linguagem novo seria justamente usar corrotinas e então colocar uma camada sintática de efeitos sobre a estrutura básica de yield/resume.
    Pergunto se estou deixando passar algum detalhe.

    • Uma diferença marcante entre AE e corrotinas seria a segurança de tipos.
      Em AE, dá para explicitar no código-fonte quais efeitos uma função pode usar.
      Por exemplo, se query_db(): User can Database, isso indica que a função pode acessar o banco de dados e que, ao chamá-la, é obrigatório fornecer um tratador Database.
      Fica muito claro quais ações são permitidas ou não.
      Assim como componentes de servidor em NextJS não podem usar diretamente funcionalidades de cliente, esse tipo de restrição de segurança é popular em várias áreas.

    • O Effect-TS chega perto dessa abordagem em JavaScript, usando corrotinas, mas não tenho certeza de que isso resulta mesmo em uma boa ideia.
      Assim como DI no framework Spring, existe o risco de AE se espalhar pelo código inteiro e acabar gerando apenas mais complexidade.
      Na prática, as palestras do EffectDays sobre uso de efeitos no frontend pareciam ser, em sua maioria, só boilerplate sem sentido.
      AE é um conceito fascinante, mas a necessidade de encapsular muita coisa em funções pode prejudicar a praticidade típica do JavaScript para escrever código com rapidez.
      Por outro lado, abordagens como a do MotionCanvas, que usam apenas corrotinas para expressar cenários complexos de gráficos 2D com facilidade, têm grandes vantagens.
      Vídeo relacionado do EffectDays
      MotionCanvas

    • Há quem diga que, dentro de uma thread, tratadores de AE podem fazer resume do código várias vezes, de forma parecida com call/cc.
      Já no caso de corrotinas, cada yield só pode ser retomado uma vez.
      Como esse fluxo de execução mais incerto acaba dificultando a previsibilidade, eu prefiro abordagens em que funções que podem ser chamadas várias vezes retornam isso explicitamente, ou então usar outras estruturas, como iteradores.

  • Como abstração de programação, isso me parece extremamente atraente.
    Quando trabalhei com programação de kernel na Sun, eu sentia uma grande vantagem em poder escrever código de forma concisa após uma chamada como sleep(foo) e retomar a partir dali quando foo me acordasse novamente.
    Isso reduz a carga de ter que tratar vários casos de borda manualmente no fluxo de controle.
    Se tomar cuidado apenas com questões de localidade de memória, parece divertido inicializar várias funções em estado de espera e expressar o algoritmo diretamente como a mutação de cada unidade.

  • Sobre a afirmação de que “efeitos algébricos são como exceções que podem ser retomadas”:
    pergunto em que isso difere, na prática, de typeclasses como ApplicativeError ou MonadError.
    A forma de explicitar os efeitos que uma função pode usar lembra checked exceptions, e tratar efeitos com expressões handle parece quase o mesmo que try/catch.
    Essas typeclasses já oferecem formas de capturar exceções com coisas como handleError e handleErrorWith.
    Diz-se que efeitos algébricos teriam vantagens para linguagens “do futuro”, mas na prática parecem ser conceitos que já são usados hoje.
    Explicação do cats

    • Se estiver lidando com apenas um efeito, a diferença talvez não seja tão grande.
      Mas quando vários efeitos são necessários ao mesmo tempo, suporte direto a efeitos é muito mais limpo e intuitivo do que explicitar mônadas aninhadas.
      Ao combinar mônadas, surgem problemas incômodos como definir a ordem ou reorganizá-las quando o conjunto esperado por parte das funções não coincide com o resultado produzido.

    • Pessoalmente, acho mais adequado ver mônadas e efeitos não como concorrentes, mas como interpretações complementares.
      Veja, por exemplo, este artigo relacionado: artigo sobre Koka

    • Efeitos algébricos operam na stack do programa como continuations delimitadas.
      Só com truques simples de mônadas, não dá para saltar imediatamente para um tratador de efeito que está 5 frames acima na stack, alterar apenas variáveis locais naquele frame e depois voltar 5 níveis para baixo.

    • A diferença está no comportamento estático vs. dinâmico.
      Ao programar com mônadas, é preciso implementar diretamente todos os métodos relacionados, enquanto em um sistema de efeitos é possível instalar tratadores dinamicamente em qualquer ponto e sobrescrever tratadores existentes com flexibilidade.
      Por exemplo, dá para usar internamente uma mônada dedicada com características de IO para testes e instalar tratadores de efeito apenas abaixo dela.

    • Há muitas semelhanças, mas a usabilidade é diferente.
      Efeitos algébricos lembram uma free monad, mas, por serem embutidos na linguagem, têm sintaxe mais simples e melhor composabilidade.
      Em linguagens centradas em mônadas, como Haskell, é possível obter algo superficialmente parecido graças à inferência de typeclasses no estilo mtl e à sintaxe embutida de bind.

  • Eu achava que efeitos algébricos eram tratados apenas em sistemas de tipos estáticos, mas recentemente descobri que também existem estruturas dinâmicas nessa linha.
    Dois textos antigos sobre a versão dinâmica de Eff me impressionaram bastante (primeiro, segundo).
    Conceitos como “operações parametrizadas com aridade generalizada” também me parecem uma parte interessante de ligar abstração à programação.

    • O que exatamente você não gosta em sistemas de tipos estáticos?
  • Foi mencionado que se trata de uma ideia antiga reaparecendo recentemente com um novo nome e uma nova moldura.
    Introdução ao LISP Condition System
    Experiência prática com Algebraic Effects

  • Usei effects no alpha do OCaml 5 para fazer Protohackers.
    Foi divertido, embora o toolchain naquela época fosse um pouco incômodo.
    Como Ante passa uma sensação parecida, fico na expectativa da evolução daqui para frente.

    • Desde o OCaml 5.3, os effects estão muito melhores do que antes.
      Ainda não têm um sistema de tipos acoplado, mas agora certamente estão bem mais limpos.
  • Passei muito tempo com Prolog e estou procurando uma linguagem que facilite combinar funções não determinísticas com verificação de tipos em tempo de compilação.
    Ante é um dos candidatos que despertam meu interesse.
    Também não se pode esquecer de ferramentas para desenvolvedor e plugins de editor, como LSP e tree-sitter.

    • Como autor de Ante, já há suporte a LSP, embora ainda seja bem básico.
      Acho essencial que uma linguagem nova tenha tooling desde cedo.
      Também dou bastante importância à experiência de depuração, então estou considerando se ao menos no modo debug seria possível oferecer replayability por padrão.
  • Sobre a afirmação de que “efeitos algébricos são como exceções que podem ser retomadas”:
    alguém perguntou se isso seria semelhante às conditions do Common Lisp.
    Achei interessante ver como ideias antigas voltam com outro nome.

    • Efeitos algébricos são muito mais abrangentes do que o condition system de LISP.
      Como as continuations podem ser multi-shot, isso os aproxima de call/cc em Scheme.
      Também foi mencionado que esse tipo de paralelismo pode acabar trazendo resultados piores do que não tê-lo.

    • Smalltalk tem “exceções retomáveis” (resumable exceptions).

    • Se tratarmos efeitos apenas como uma renomeação de antigos condition systems, fica difícil avançar na discussão.
      Os efeitos algébricos discutidos hoje têm diferenças que vão além de um conceito simples.

    • Dependency Injection também pode ser citada em um contexto parecido.