Fórmulas e Combinadores Preguiçosos

Biblioteca de fórmulas


Na fintech, geralmente precisamos verificar se condições aritméticas simples são satisfeitas, por exemplo, se a taxa de câmbio será maior que o valor esperado ou não. Essas condições mudam com muita frequência, e precisávamos inventar algum tipo de bicicleta para adicionar novas verificações e executar as existentes em tempo real. Imagine que vários milhares de clientes esperam receber notificações quando a taxa de câmbio de alguns pares de moedas atingir uma proporção de dois para um. Seria muito simples se pudéssemos tornar as condições estáticas:

def notify?(rate) when rate > 2.0, do: true def notify?(_), do: false 

Permitimos que os clientes adicionem tais verificações dinamicamente. Portanto, precisamos de um mecanismo mais ou menos confiável para verificar as condições recém-adicionadas.

Sim, o Code.eval_string/3 funciona de alguma forma, mas compila a condição a cada momento antes de verificar. Obviamente, isso é um desperdício de recursos sem motivo. O que é agravado pelo fato de recebermos e processarmos cerca de 10.000 cursos para diferentes pares de moedas a cada segundo.

Então, criamos fórmulas pré-compiladas. A pequena biblioteca de formulae cria um módulo para cada condição especificada e compila a fórmula inserida pelo usuário no código - uma vez.

A biblioteca NB deve ser usada com cautela, porque os nomes dos módulos são armazenados na forma de átomos e sua criação incondicional cega para tudo o que o cliente deseja verificar pode levar a um ataque de DoS atômico na máquina virtual Erlang com um tempo de execução mais ou menos longo. Usamos a etapa máxima permitida de um valor de 0.01 , que fornece um máximo de 200 mil átomos no pior cenário.

Lazy Combinators


Mas o principal objetivo deste artigo não é falar sobre fórmulas pré-compiladas. Para alguns casos limítrofes de análise da taxa de câmbio, precisávamos calcular as permutações de uma lista bastante longa. De repente, a biblioteca padrão do Elixir não fornece uma solução pronta para uso. Bem, eu decidi copiar as assinaturas combinatórias do Ruby ( Array#combination e primos). Infelizmente, isso não foi tão fácil para longas listas. As combinações pararam na região de trinta elementos da lista, permutação - ainda mais cedo.

Bem, espera-se que já esteja aqui; então comecei a jogar uma implementação lenta usando o Stream. Acabou, e não é tão fácil quanto eu pensava. Eu vim com algo como o código abaixo

 list = ~w[abcde]a combinations = Stream.transform(Stream.with_index(list), :ok, fn {i1, idx1}, :ok -> {Stream.transform(Stream.with_index(list), :ok, fn {_, idx2}, :ok when idx2 <= idx1 -> {[], :ok} {i2, idx2}, :ok -> {Stream.transform(Stream.with_index(list), :ok, fn {_, idx3}, :ok when idx3 <= idx2 -> {[], :ok} {i3, idx3}, :ok -> {Stream.transform(Stream.with_index(list), :ok, fn {_, idx4}, :ok when idx4 <= idx3 -> {[], :ok} {i4, _idx4}, :ok -> {[[i1, i2, i3, i4]], :ok} end), :ok} end), :ok} end), :ok} end) 

Isso funciona, mas apenas para um número conhecido de combinações. Bem, isso é facilmente superável: nesse caso, temos macros, certo?

No código acima, três padrões diferentes são exibidos. Uma ramificação bem-sucedida da qual descartamos a lista. Ejeção rápida de uma lista vazia. E a transformação do fluxo com o índice. Parece que poderíamos tentar criar um AST para o acima.

Este é um caso raro ao usar o Kernel.SpecialForms.quote/2 provavelmente só complicará as coisas, por isso tomei o caminho de menor resistência: esculpiremos o bom e velho AST nu.

Comecei invocando quote do: no console desse código e, direto ao ponto, examinei o resultado. Sim, existem padrões. Bem, vamos lá.

Então, você precisa começar criando uma estrutura comum.

 defmacrop mapper(from, to, fun), do: quote(do: Enum.map(Range.new(unquote(from), unquote(to)), unquote(fun))) @spec combinations(list :: list(), count :: non_neg_integer()) :: {Stream.t(), :ok} defmacro combinations(l, n) do Enum.reduce(n..1, {[mapper(1, n, &var/1)], :ok}, fn i, body -> stream_combination_transform_clause(i, l, body) end) end 

Agora precisamos começar a pensar em termos não de código, mas de AST, para ver partes repetidas do modelo. Isso é divertido!

Vamos começar com macros auxiliares para simplificar o código:

 def var(i), do: {:"i_#{i}", [], Elixir} def idx(i), do: {:"idx_#{i}", [], Elixir} 

Peça interna AST rasgada de uma visão geral:

 def sink_combination_clause(i) when i > 1 do {:->, [], [ [ {:when, [], [ {​{:_, [], Elixir}, idx(i)}, :ok, {:<=, [context: Elixir, import: Kernel], [idx(i), idx(i - 1)]} ]} ], {[], :ok} ]} end 

Todas as peças internas juntas:

 def sink_combination_clauses(1, body) do [{:->, [], [[{var(1), idx(1)}, :ok], body]}] end def sink_combination_clauses(i, body) when i > 1 do Enum.reverse([ {:->, [], [[{var(i), idx(i)}, :ok], body]} | Enum.map(2..i, &sink_combination_clause/1) ]) end 

E, finalmente, o invólucro externo em torno de tudo.

 def stream_combination_transform_clause(i, l, body) do clauses = sink_combination_clauses(i, body) {​{​{:., [], [{:__aliases__, [alias: false], [:Stream]}, :transform]}, [], [ {​{:., [], [{:__aliases__, [alias: false], [:Stream]}, :with_index]}, [], [l]}, :ok, {:fn, [], clauses} ]}, :ok} end 

Todas as permutações são realizadas quase de forma idêntica, a única alteração é a condição nas chamadas internas. Foi fácil, né? Todo o código pode ser visualizado no repositório .

App


Bom, então como podemos usar essa beleza? Bem, algo como isto:

 l = for c <- ?a..?z, do: <<c>> # letters list with {stream, :ok} <- Formulae.Combinators.Stream.permutations(l, 12), do: stream |> Stream.take_every(26) |> Enum.take(2) #⇒ [["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"], # ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "l", "w"]] 

Agora podemos alimentar o Flow diretamente desse fluxo para paralelizar os cálculos. Sim, ainda será lento e triste, mas, felizmente, essa tarefa não é em tempo real, mas para análises que podem ser executadas à noite e que passam lentamente por todas as combinações e anotam os resultados em algum lugar.

Se você tiver dúvidas sobre o AST no Elixir - pergunte, eu comi um cachorro nele.

Source: https://habr.com/ru/post/pt477222/


All Articles