Por que precisamos de efeitos algébricos
(antelang.org)- Efeitos algébricos são um recurso de linguagem que captura e trata o fluxo de controle como exceções retomáveis; são um recurso central do Ante e também são usados de forma central em linguagens de pesquisa como Koka, Effekt, Eff e Flix
- Com o mesmo mecanismo, é possível criar generators, exceções, async, corrotinas e diferenciação automática no nível de biblioteca, e graças ao polimorfismo de efeitos, funções como
mappodem ser escritas apenas uma vez, independentemente do tipo de efeito - Se injeção de dependência e passagem de contexto, como acesso a banco de dados, saída, logging e passagem de estado, forem transformadas em efeitos, mocks de teste, coleta de saída e filtragem de logs podem ser tratadas trocando handlers
- Quando efeitos como
can IO,can Printecan Failaparecem na assinatura da função, isso é vantajoso para garantir pureza, gravação/reprodução e auditoria de segurança, embora efeitos já permitidos possam se propagar sem querer para handlers existentes - A fraqueza tradicional era a preocupação com eficiência, mas linguagens recentes vêm reduzindo o custo com otimizações para efeitos tail-resumptive, evidence passing, limitação a um único
resumee especialização de handlers
Modelo básico de efeitos algébricos
- Efeitos algébricos também são chamados de effect handlers e podem ser entendidos pelo modelo de “exceções retomáveis”
- No pseudocódigo de Ante, declara-se uma função de efeito e marca-se na assinatura da função, com
can, que aquele efeito pode ser usado- Chamar uma função de efeito como
say_message: Unit -> Unitequivale a “lançar” o efeito - A função chamadora expõe essa possibilidade na assinatura, como em
foo () can SayMessage
- Chamar uma função de efeito como
- A expressão
handlecaptura o efeito de forma parecida comtry/catche continua o cálculo interrompido ao chamarresume- Se o handler de
say_messageexecutarprint "Hello World!"e depois chamarresume (), o cálculo original continua e retorna42
- Se o handler de
- O nome “algébrico” é, em grande parte, um termo legado; na prática, effect handlers é uma descrição mais precisa, mas usa-se “efeitos algébricos” por ser um nome mais familiar para os usuários
Fluxo de controle definido pelo usuário
- Efeitos algébricos permitem implementar vários recursos de linguagem com um único mecanismo
- generators
- exceções
- async
- corrotinas
- diferenciação automática
- O polimorfismo de efeitos reduz o problema de what color is your function
map (input: Vec a) (f: a -> b can e): Vec b can eexpressa que, qualquer que seja o efeitoerealizado pela função de entradaf,maptambém realiza o mesmo efeito- O mesmo
mappode ser usado junto com saída em stdout, chamadas de função assíncronas eyieldde stream - Muitas linguagens com effect handlers omitem a variável de efeito
e, permitindo usar a forma familiarmap (input: Vec a) (f: a -> b): Vec b
- Exceções podem ser implementadas tratando o efeito de forma a não chamar
resume- Define-se o efeito
Throw acomthrow: a -> never_returns - Ao dividir por zero, chama-se
throw "error: Division by zero!", e o handler imprime a mensagem sem retomar o cálculo
- Define-se o efeito
- Generators podem ser implementados com o efeito
Yield aeyield: a -> Unit- Percorre-se os elementos de um vetor chamando
yield elem - O handler de
filterchamayield xnovamente quando o valor produzido satisfaz a condição e segue para o próximo elemento comresume () - O handler de
my_for_eachexecuta a funçãofpara cada valor produzido e continua comresume ()
- Percorre-se os elementos de um vetor chamando
- Um escalonador cooperativo também pode ser criado com o efeito
yield: Unit -> Unit, em que o handler assume o controle e alterna para a execução de outra função- O exemplo de scheduler do Effekt mostra esse padrão
- Vários efeitos se compõem bem entre si, e isso é apontado como uma vantagem de usabilidade em relação a outras abstrações de efeito
Injeção de dependência e testabilidade
- Efeitos também podem ser usados em aplicações de negócio comuns para injeção de dependência
- Em vez de passar diretamente um objeto de banco de dados como argumento, pode-se definir um efeito
Database- No formato tradicional, algo como
business_logic (db: Database) (x: I32)recebe o objeto de banco como argumento - Na forma baseada em efeitos, fica
business_logic (x: I32) can Database, e internamente a função chamaquery "..."
- No formato tradicional, algo como
- A escolha do banco de dados concreto fica a cargo de um handler em um nível mais alto da pilha de chamadas
- É possível trocar o banco de produção por outro ou substituí-lo por um mock para testes
- Um handler
mock_databasepode ignorar a mensagemquerye sempre fazerresumecomDbResponse.Ok
- Se a saída também for tratada como efeito, durante os testes é possível coletá-la como string sem escrever diretamente em stdout
- Um handler
print_to_stringcaptura chamadasprint msge as acumula na stringall_messagescom quebras de linha output_messagespode validar o valor de retorno1234e a string de mensagens sem saída real
- Um handler
- Logging pode ser transformado em saída condicional usando o efeito
LogeLogLevellog_handlerchamaprint msgquando o nível da mensagem é igual ou superior ao limite configuradofoo () with log_handler Errorimprime apenas logs de erro
APIs mais limpas e passagem de contexto
- Efeitos algébricos permitem representar com efeitos o padrão de objeto de Context propagado por todo o programa ou biblioteca
- O efeito
Use apode ser visto como um efeito de estado, oferecendoget: Unit -> aeset: a -> Unit- O handler
stateguarda o estado inicial, retorna o contexto atual emgete o atualiza para o novo contexto emset - A definição de exemplo de
stateignora regras de ownership, e uma implementação real pode exigir a restriçãoCopy a
- O handler
- O exemplo de armazenar strings em um vetor e passar índices como chaves mostra o custo de propagar contexto
- Sem efeitos,
push_string,get_string,append_with_separatoreexampleprecisam continuar recebendostringscomo argumento - Na implementação baseada em efeitos, as operações primitivas
push_stringeget_stringchamamget/set, e o código de nível superior não precisa passarstringsdiretamente
- Sem efeitos,
- Essa abordagem se encaixa bem quando uma biblioteca encapsula a passagem de contexto interna
- O usuário da biblioteca não precisa se preocupar com os detalhes internos de como o contexto é propagado
- Para não ficar preso a um tipo específico de contexto, é possível abstrair as funções necessárias por meio de interfaces
Substituição de variáveis globais e estilo direto
- APIs como geração de números aleatórios ou alocação de memória, que parecem sem estado por fora mas exigem estado na prática, podem ser expressas com efeitos em vez de variáveis globais
- O exemplo de geração aleatória mostra o incômodo de ter de propagar manualmente um objeto
Prngpor todo o programa- Um
Prngglobal é conveniente, mas traz as desvantagens de valores globais, como a necessidade de thread safety - Com
random: Unit -> U8do efeitoRandom, o usuário só precisa deixar explícito que em algum ponto acima na pilha de chamadas o efeito será inicializado por um handler - Depois, para trocar por
/dev/urandomou outra fonte de aleatoriedade, basta substituir o handler, sem mudar o restante da pilha de chamadas
- Um
- A alocação de memória também pode ser expressa com o efeito
Allocateallocate: (size: Usz) -> Alignment -> Ptr afree: Ptr a -> Unit- Na maioria das chamadas usa-se o allocator global, e em um tight loop é possível adicionar um handler ao corpo do loop para trocar por um arena allocator
- Efeitos possibilitam estilo direto em vez de passar resultados embrulhados em valores dedicados
- Com
Maybe t, é preciso encadear o caminho de sucesso comand_thenemap - Açúcar sintático como o
?do Rust existe para manter o foco no caminho feliz - Em uma versão baseada em efeitos,
get_line_from_stdin (): String can Fail, IOeparse (s: String): U32 can Failpodem ser escritos como código sequencial comum, comline = ...,x = ...,x * 2
- Com
- O tratamento de falha pode ser feito aplicando um handler para sair do caminho principal quando necessário
get_line_from_stdin () with default "42"trata o efeitoFailcom um valor padrão
- Tipos de erro diferentes também se compõem naturalmente como listas de efeitos
LibraryA.foo (): U32 can Throw LibraryA.ErrorLibraryB.bar (): U32 can Throw LibraryB.Errormy_functionpode declarar juntosThrow LibraryA.Error,Throw LibraryB.ErroreThrow MyError- Se a repetição ficar longa, é possível criar um alias de tipo como
AllErrors = can Throw ... - O mesmo efeito
Throw Stringé unificado em um só; se quiser separá-los, é preciso um tipo wrapper comoMyError
Pureza, reexecução e auditoria de segurança
- A maioria das linguagens com effect handlers, com exceção aproximada de OCaml, usa efeitos em todos os lugares onde efeitos colaterais podem ocorrer
- Em Ante, sem algo como
can Printoucan IO, não é possível usar efeitos colaterais - Definições
externnão podem ser verificadas pelo compilador, então é preciso confiar na definição de tipo - Há planos para permitir executar o efeito
IOapenas em modo debug, preservando a segurança de efeitos no modo release
- Em Ante, sem algo como
- Algumas funções exigem uma função pura como entrada
- Ao criar uma thread, a thread criada não deve poder chamar handlers que pertencem à thread atual
spawn_all (functions: Vec (Unit -> a pure)): Vec a can IOaceita apenas funções puras, executa todas em threads e espera sua conclusão
- Software Transactional Memory (STM) é uma técnica de concorrência que requer funções puras
- Ao executar várias funções simultaneamente, se durante a transação um valor for alterado por outra thread, a transação correspondente é reiniciada
- O protótipo de prova de conceito do Effekt está em effekt-stm
- A pureza também pode oferecer possibilidades de gravação/reprodução semelhantes ao utilitário de depuração
rr- Dois handlers,
recordereplay, tratam os efeitos de nível mais alto exportados pormain, normalmenteIO recordgrava a ocorrência do efeito e seu resultado, depois repassa para o handlerIOembutido para processamento realreplaynão executa oIOreal e usa os resultados do log de efeitos- Gravar por padrão em builds de debug pode permitir depuração determinística
- Dois handlers,
- A lista de efeitos na assinatura da função ajuda na auditoria de segurança, de forma semelhante a Capability Based Security
get_pi: Unit -> F64permite saber que a função não está fazendoIOescondido em segundo plano- Se após uma atualização de biblioteca
get_pi: Unit -> F64 can IO, então, a menos que a função chamadora já exijaIO, o código passará a apresentar erro - O ideal é declarar apenas os efeitos mínimos necessários; por exemplo, declarar
Printem vez deIOcompleto - Adicionar um novo efeito é tratado como uma mudança que quebra semantic versioning
- Materiais relacionados incluem Capability Based Security e Designing with Static Capabilities and Effects
Limites e estratégias de implementação
- Uma das limitações da abordagem com efeitos é a possibilidade de tratamento não intencional
- Se uma função passar a exigir
IO, a função chamadora pode não gerar erro caso já permitaIO - O mesmo vale para o efeito
Fail: se uma função de biblioteca que antes não falhava passar a poder falhar, ela pode se propagar para um handlerFailjá existente - Esse comportamento pode ser aceitável dependendo do caso, mas pode divergir da intenção quando se queria um tratamento separado, como fornecer um valor padrão
- Se uma função passar a exigir
- A principal desvantagem tradicional era a preocupação com eficiência, mas a saída compilada dos efeitos melhorou bastante nos últimos tempos
- Muitas linguagens de efeitos algébricos otimizam efeitos tail-resumptive como chamadas normais de closure
- Efeitos tail-resumptive são aqueles em que o handler chama
resumepor último - A maioria dos efeitos reais se enquadra aqui, e a maior parte dos exemplos do texto também está nessa categoria
- Exceções, por não chamarem
resumede forma alguma, são tratadas como caso à parte
- Efeitos tail-resumptive são aqueles em que o handler chama
- As estratégias de otimização variam entre as linguagens
- Koka usa evidence passing e eleva os efeitos até seus handlers para compilar para C sem runtime
- Ante e OCaml limitam
resumea no máximo uma chamada- Essa restrição exclui alguns efeitos, como não determinismo
- Em compensação, simplifica o gerenciamento de recursos e permite implementar continuações internas com mais eficiência, por exemplo com segmented stacks
- Effekt especializa completamente os handlers e os remove do programa
- Essa abordagem impõe a limitação de que a maioria das funções seja second-class
- É possível obter funções first-class em forma boxed e migrar para isso em um modelo pay-as-you-go
- Materiais relacionados incluem a documentação de captures do Effekt e o artigo
Ainda não há comentários.