Comentário do tradutor: Esta é uma tradução de um ótimo artigo de Dan Abramov, colaborador do React. Seus exemplos foram escritos para JS, mas serão igualmente claros para os desenvolvedores em qualquer idioma. A ideia é comum a todos.
Você já ouviu falar sobre efeitos algébricos?
Minhas primeiras tentativas de descobrir quem são e por que deveriam me excitar não tiveram êxito. Encontrei vários PDFs , mas eles me confundiram ainda mais. (Por alguma razão, adormeço ao ler artigos acadêmicos.)
Mas meu colega Sebastian continuou a chamá- los de modelo mental de algumas das coisas que fazemos no React. (Sebastian trabalha na equipe React e apresentou muitas idéias, incluindo Hooks e Suspense.) Em algum momento, tornou-se um meme local na equipe React, e muitas de nossas conversas terminaram com o seguinte:
Aconteceu que os efeitos algébricos são um conceito interessante, e não é tão assustador quanto me pareceu a princípio depois de ler esses PDFs. Se você acabou de usar o React, não precisa saber nada sobre eles, mas se você, como eu, estiver interessado, continue lendo.
(Isenção de responsabilidade: eu não sou um pesquisador no campo de linguagens de programação e pode ter estragado algo em minha explicação. Então, deixe-me saber se estou errado!)
Ainda é cedo na produção
Efeitos algébricos são atualmente um conceito experimental do campo de estudo de linguagens de programação. Isso significa que, diferentemente de if
, for
ou mesmo async/await
expressões, você provavelmente não poderá usá-las agora na produção. Eles são suportados por apenas alguns idiomas que foram criados especificamente para estudar essa idéia. Há progresso em sua implementação no OCaml, que ... ainda está em andamento . Em outras palavras, observe, mas não toque com as mãos.
Por que deveria me incomodar?
Imagine que você está escrevendo código usando goto
, e alguém está lhe dizendo sobre a existência de construções if
e for
. Ou talvez você esteja atolado em um inferno de retorno de chamada e alguém esteja lhe mostrando async/await
. Muito legal, não é?
Se você é do tipo de pessoa que gosta de aprender inovações de programação alguns anos antes de se tornar moda, talvez seja hora de se interessar por efeitos algébricos. Embora não seja necessário. É assim que se fala sobre async/await
em 1999.
Bem, que tipo de efeitos são esses?
O nome pode ser um pouco confuso, mas a ideia é simples. Se você estiver familiarizado com try/catch
blocos try/catch
, entenderá rapidamente os efeitos algébricos.
Vamos relembrar try/catch
. Digamos que você tenha uma função que lança exceções. Talvez haja várias chamadas aninhadas entre ele e o catch
:
function getName(user) { let name = user.name; if (name === null) { throw new Error(' '); } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } catch (err) { console.log(", : ", err); }
Lançamos uma exceção dentro de getName
, mas ela aparece através do makeFriends
até o catch
mais próximo. Essa é a principal propriedade do try/catch
. O código intermediário não é necessário para se preocupar com o tratamento de erros.
Ao contrário dos códigos de erro em idiomas como C, ao usar o try/catch
você não precisa passar manualmente os erros por cada nível intermediário para lidar com o erro no nível superior. Exceções aparecem automaticamente.
O que isso tem a ver com efeitos algébricos?
No exemplo acima, assim que virmos um erro, não poderemos continuar executando o programa. Quando nos encontramos em um catch
, a execução normal do programa para.
Está tudo acabado. É tarde demais. O melhor que podemos fazer é nos recuperar do fracasso e, de alguma forma, repetir o que estávamos fazendo, mas não podemos magicamente “voltar” para onde estávamos e fazer outra coisa. E com efeitos algébricos, nós podemos.
Este é um exemplo escrito em um dialeto JavaScript hipotético (vamos chamá-lo de ES2025 por diversão), que nos permite continuar trabalhando após a falta de user.name
:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
(Peço desculpas a todos os leitores de 2025 que pesquisam na Internet "ES2025" e se enquadram neste artigo. Se, a essa altura, os efeitos algébricos se tornarem parte do JavaScript, ficarei feliz em atualizar o artigo!)
Em vez de throw
usamos a palavra-chave hipotética perform
. Da mesma forma, em vez de try/catch
usamos o try/handle
hipotético. A sintaxe exata não importa aqui - apenas inventei algo para ilustrar a idéia.
Então, o que está acontecendo aqui? Vamos dar uma olhada.
Em vez de gerar um erro, realizamos o efeito . Assim como podemos jogar qualquer objeto, aqui podemos passar algum valor para o processamento . Neste exemplo, passo uma string, mas ela pode ser um objeto ou qualquer outro tipo de dados:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; }
Quando lançamos uma exceção, o mecanismo procura o manipulador de try/catch
mais próximo na pilha de chamadas. Da mesma forma, quando executamos um efeito , o mecanismo procurará o try/handle
efeito try/handle
mais próximo no topo da pilha:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Esse efeito nos permite decidir como lidar com a situação quando o nome não for especificado. Novo aqui (comparado às exceções) é o resume with
hipotético resume with
:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Isso é algo que você não pode fazer com o try/catch
. Isso nos permite voltar para onde realizamos o efeito e passar algo do manipulador . : -O
function getName(user) { let name = user.name; if (name === null) {
Demora um pouco para ficar confortável, mas conceitualmente isso não é muito diferente de try/catch
com um retorno.
Observe, no entanto, que os efeitos algébricos são uma ferramenta muito mais poderosa do que apenas try/catch
. A recuperação de erros é apenas um dos muitos casos de uso possíveis. Comecei com este exemplo apenas porque era mais fácil para mim entender.
Função não tem cor
Efeitos algébricos têm implicações interessantes para código assíncrono.
Em idiomas com async/await
funções geralmente têm uma "cor" ( russo ). Por exemplo, no JavaScript, não podemos simplesmente tornar getName
assíncrono sem infectar o makeFriends
e suas funções de chamada com async. Isso pode ser um problema real se parte do código às vezes precisar ser síncrona e, às vezes, assíncrona.
Os geradores JavaScript funcionam de maneira semelhante : se você trabalha com geradores, todo o código intermediário também deve saber sobre geradores.
Bem, o que isso tem a ver com isso?
Por um momento, vamos esquecer o assíncrono / aguardar e voltar ao nosso exemplo:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
E se o nosso manipulador de efeitos não puder retornar o "nome reserva" de forma síncrona? E se quisermos obtê-lo do banco de dados?
Acontece que podemos chamar resume with
assincronamente do nosso manipulador de efeitos sem fazer alterações em getName
ou makeFriends
:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { setTimeout(() => { resume with ' '; }, 1000); } }
Neste exemplo, chamamos resume with
apenas um segundo depois. Você pode considerar resume with
retorno de chamada, o qual você pode chamar apenas uma vez. (Você também pode se mostrar aos amigos chamando isso de "uma continuação limitada única" (o termo continuação delimitada ainda não recebeu uma tradução estável para o russo - aprox. Transl.).)
Agora a mecânica dos efeitos algébricos deve ser um pouco mais clara. Quando lançamos um erro, o mecanismo JavaScript gira a pilha destruindo variáveis locais no processo. No entanto, quando executamos o efeito, nosso mecanismo hipotético cria um retorno de chamada (na verdade, um “quadro de continuação”, aprox. Transl.). Com o restante de nossa função, e resume with
a chamada.
Novamente, um lembrete: a sintaxe específica e as palavras-chave específicas são inteiramente inventadas apenas para este artigo. O ponto não está nele, mas na mecânica.
Nota de limpeza
Vale ressaltar que os efeitos algébricos surgiram como resultado do estudo da programação funcional. Alguns dos problemas que eles resolvem são exclusivos apenas da programação funcional. Por exemplo, em idiomas que não permitem efeitos colaterais arbitrários (como Haskell), você deve usar conceitos como mônadas para arrastar efeitos pelo seu programa. Se você já leu o tutorial da mônada, sabe que pode ser difícil de entender. Efeitos algébricos ajudam a fazer algo semelhante com um pouco menos de esforço.
É por isso que a maioria das discussões sobre efeitos algébricos são completamente incompreensíveis para mim. ( Não conheço Haskell e seus "amigos".) No entanto, acho que mesmo em uma linguagem impura como JavaScript, os efeitos algébricos podem ser uma ferramenta muito poderosa para separar o "o quê" do "como" no seu código.
Eles permitem que você escreva um código que descreva o que você está fazendo:
function enumerateFiles(dir) { const contents = perform OpenDirectory(dir); perform Log('Enumerating files in ', dir); for (let file of contents.files) { perform HandleFile(file); } perform Log('Enumerating subdirectories in ', dir); for (let directory of contents.dir) {
E depois envolva-o com algo que descreve o "como" você faz:
let files = []; try { enumerateFiles('C:\\'); } handle(effect) { if (effect instanceof Log) { myLoggingLibrary.log(effect.message); resume; } else if (effect instanceof OpenDirectory) { myFileSystemImpl.openDir(effect.dirName, (contents) => { resume with contents; }); } else if (effect instanceof HandleFile) { files.push(effect.fileName); resume; } }
O que significa que essas partes podem se tornar uma biblioteca:
import { withMyLoggingLibrary } from 'my-log'; import { withMyFileSystem } from 'my-fs'; function ourProgram() { enumerateFiles('C:\\'); } withMyLoggingLibrary(() => { withMyFileSystem(() => { ourProgram(); }); });
Ao contrário de assíncrono / espera ou geradores, efeitos algébricos não exigem a complicação de funções "intermediárias". Nossa chamada para enumerateFiles
pode estar dentro do nosso programa, mas, desde que em algum lugar acima exista um manipulador de efeitos para cada um dos efeitos que ele pode executar, nosso código continuará funcionando.
Os manipuladores de efeitos nos permitem separar a lógica do programa das implementações específicas de seus efeitos sem danças e códigos desnecessários. Por exemplo, podemos redefinir completamente o comportamento nos testes para usar o sistema de arquivos falso e fazer instantâneos de logs em vez de exibi-los no console:
import { withFakeFileSystem } from 'fake-fs'; function withLogSnapshot(fn) { let logs = []; try { fn(); } handle(effect) { if (effect instanceof Log) { logs.push(effect.message); resume; } }
Como as funções não possuem uma "cor" (o código intermediário não precisa saber sobre efeitos) e os manipuladores de efeitos podem ser compostos (eles podem ser aninhados), você pode criar abstrações muito expressivas com elas.
Tipos Nota
Como os efeitos algébricos vêm de linguagens estaticamente tipificadas, a maior parte do debate sobre eles se concentra em como expressá-los em tipos. Isso é sem dúvida importante, mas também pode complicar a compreensão do conceito. É por isso que este artigo não fala sobre tipos. No entanto, devo observar que geralmente o fato de uma função poder executar um efeito será codificado em uma assinatura de seu tipo. Assim, você estará protegido de uma situação em que efeitos imprevisíveis são executados ou não pode rastrear de onde eles vêm.
Aqui você pode dizer que os efeitos tecnicamente algébricos “dão cor” às funções em linguagens estaticamente tipadas, uma vez que os efeitos fazem parte de uma assinatura de tipo. É mesmo. No entanto, corrigir a anotação de tipo para uma função intermediária para incluir um novo efeito não é, por si só, uma mudança semântica - ao contrário de adicionar assíncrono ou transformar uma função em um gerador. A inferência de tipo também pode ajudar a evitar a necessidade de alterações em cascata. Uma diferença importante é que você pode "suprimir" os efeitos inserindo um stub vazio ou uma implementação temporária (por exemplo, uma chamada de sincronização para um efeito assíncrono), que, se necessário, permite impedir o efeito no código externo - ou transformá-lo em outro efeito.
Preciso de efeitos algébricos em JavaScript?
Honestamente, eu não sei. Eles são muito poderosos, e pode-se argumentar que eles são muito poderosos para uma linguagem como o JavaScript.
Eu acho que eles podem ser muito úteis para linguagens onde a mutabilidade é rara e onde a biblioteca padrão suporta totalmente os efeitos. Se você executar pela primeira vez o perform Timeout(1000), perform Fetch('http://google.com')
e perform ReadFile('file.txt')
, e seu idioma terá “correspondência de padrão” e digitação estática para efeitos, esse pode ser um ambiente de programação muito agradável.
Talvez essa linguagem seja compilada em JavaScript!
O que isso tem a ver com o React?
Não é muito grande. Você pode até dizer que eu puxo uma coruja em um globo.
Se você assistiu à minha conversa sobre Time Slicing and Suspense, a segunda parte inclui componentes que lêem dados do cache:
function MovieDetails({ id }) {
(O relatório usa uma API um pouco diferente, mas não é esse o ponto.)
Esse código é baseado na função React para amostras de dados denominadas " Suspense
", que está atualmente em desenvolvimento ativo. O interessante aqui, é claro, é que os dados ainda não estão no movieCache - nesse caso, precisamos fazer algo primeiro, porque não podemos continuar a execução. Tecnicamente, nesse caso, a chamada para read () lança Promise (sim, lança Promise - você precisa engolir esse fato). Isso interrompe a execução. O React intercepta esta promessa e lembra que é necessário repetir a renderização da árvore de componentes após o cumprimento da promessa lançada.
Este não é um efeito algébrico em si, embora a criação desse truque tenha sido inspirada por eles. Esse truque alcança o mesmo objetivo: parte do código abaixo na pilha de chamadas é temporariamente inferior a algo mais alto na pilha de chamadas (neste caso, React), enquanto todas as funções intermediárias não precisam saber sobre isso ou serem “envenenadas” por assíncronos ou geradores. Obviamente, não podemos "realmente" retomar a execução em JavaScript, mas do ponto de vista do React, exibir novamente a árvore de componentes após a permissão Promise ser quase a mesma. Você pode trapacear quando o seu modelo de programação assume idempotência!
Ganchos são outro exemplo que pode lembrá-lo de efeitos algébricos. Uma das primeiras perguntas que as pessoas fazem é: aonde o useState chama “sabe” a qual componente se refere?
function LikeButton() {
Eu já expliquei isso no final deste artigo : no objeto React, existe um estado mutável "despachante atual", que indica a implementação que você está usando no momento (por exemplo, como em react-dom
). Da mesma forma, existe uma propriedade atual do componente que aponta para a estrutura de dados interna do LikeButton. Veja como o useState descobre o que fazer.
Antes de se acostumar, as pessoas costumam pensar que parece um truque sujo por um motivo óbvio. É errado confiar em um estado mutável geral. (Nota: como você acha que o try / catch está implementado no mecanismo JavaScript?)
No entanto, conceitualmente, você pode considerar useState () como um efeito da execução de State (), que é processada pelo React quando seu componente é executado. Isso "explica" por que o React (o que seu componente chama) pode fornecer o estado (é mais alto na pilha de chamadas, portanto, pode fornecer um manipulador de efeitos). De fato, a implementação explícita do estado é um dos exemplos mais comuns em livros didáticos sobre efeitos algébricos que encontrei.
Novamente, é claro, não é assim que o React realmente funciona, porque não temos efeitos algébricos no JavaScript. Em vez disso, há um campo oculto no qual salvamos o componente atual, bem como um campo que aponta para o "despachante" atual com a implementação useState. Como uma otimização de desempenho, existem até implementações useState separadas para montagens e atualizações . Mas se você agora está muito distorcido por esse código, pode considerá-los manipuladores de efeitos comuns.
Resumindo, podemos dizer que no JavaScript, o throw
pode funcionar como uma primeira aproximação para efeitos de E / S (desde que o código possa ser reexecutado com segurança mais tarde, e desde que não esteja vinculado à CPU), e o campo variável " O dispatcher "restaurado em try / finalmente pode servir como uma aproximação aproximada para manipuladores de efeitos síncronos.
Você pode obter uma implementação de efeitos de qualidade muito mais alta usando geradores , mas isso significa que você deve abandonar a natureza "transparente" das funções JavaScript e precisa fazer tudo com geradores. E isso é "bem, isso ..."
Onde descobrir mais
Pessoalmente, fiquei surpreso com o sentido que os efeitos algébricos adquiriram para mim. Eu sempre tentei o meu melhor para entender conceitos abstratos, como mônadas, mas os efeitos algébricos simplesmente pegaram e "ligaram" a cabeça. Espero que este artigo os ajude a se juntar a você.
Não sei se eles começarão a ser usados a granel. Acho que ficarei desapontado se eles não se enraizarem em nenhum dos principais idiomas até 2025. Lembre-me de verificar em cinco anos!
Estou certo de que você pode fazer muito mais interessante com eles, mas é realmente difícil sentir a força deles até começar a escrever o código e usá-lo. Se este post despertou sua curiosidade, aqui estão mais alguns recursos onde você pode ler com mais detalhes:
Muitas pessoas também apontaram que, se você omitir o aspecto da digitação (como fiz neste artigo), poderá encontrar um uso anterior dessa técnica em um sistema de condições no Common Lisp. , , call/cc .