公式和惰性组合器

公式库


在金融科技领域,我们经常需要验证是否满足简单的算术条件,例如,汇率是否大于预期值。 这些条件经常变化,我们需要发明一种自行车,以便添加新检查并实时执行现有检查。 想象一下,当某些货币对的汇率达到2:1的比例时,数千客户希望收到通知。 如果我们只能使条件静态,那将是非常简单的:

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

我们允许客户动态添加此类支票。 因此,我们需要一种或多或少可靠的机制来检查刚刚添加的条件。

是的, Code.eval_string/3以某种方式可以工作,但是它在实际检查之前的每个该死的时间都会编译条件。 显然,这无缘无故浪费资源。 我们每秒接收和处理大约10,000门不同货币对的课程,这使情况更加复杂。

因此,我们提出了预编译的公式。 微小的formulae库为每个给定条件创建一个模块,并将用户输入的公式编译为代码-一次。

NB库应谨慎使用,因为模块名称以原子的形式存储,并且它们盲目无条件地创建客户端要检查的所有内容可能会导致对Erlang虚拟机的原子DoS攻击,执行时间或多或少。 我们使用的最大允许步长为0.01 ,在最坏的情况下,该步长最多为20万个原子。

懒惰组合器


但是,本文的主要目的不是讨论预编译公式。 对于汇率分析的一些极端情况,我们需要计算相当长的列表的排列。 突然,Elixir标准库没有提供交钥匙解决方案。 好吧,我决定从Ruby( Array#combination和表亲)复制组合签名。 不幸的是,对于长列表而言,这并非易事。 组合停滞在列表中的三十个元素的区域中,排列甚至更早。

好吧,预计已经在这里了; 所以我开始使用Stream玩懒惰的实现。 事实证明,这并不像我想的那么容易。 我想出了类似下面的代码

 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) 

这有效,但仅适用于已知数量的组合。 好吧,这很容易克服:在这种情况下,我们有宏,对吗?

在上面的代码中,查看了三种不同的模式。 从中删除列表的成功分支。 快速弹出一个空列表。 并用索引转换流量。 看来我们可以尝试针对上述内容创建AST。

在使用Kernel.SpecialForms.quote/2时,这种情况很少见,只会使事情复杂化,因此我采取了阻力最小的方法:我们将雕刻好的老式裸AST。

我首先在代码中的控制台中调用quote do:并检查了结果。 是的,有模式。 好吧,走吧。

因此,您需要先创建一个通用框架。

 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 

现在,我们需要开始考虑的不是代码,而是AST,以了解重复的模板部分。 很好玩!

让我们从帮助程序宏开始以简化代码:

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

AST内件从一般角度撕裂:

 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 

所有内部片段在一起:

 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 

最后,围绕着它的所有外包装。

 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 

所有排列几乎相同地执行,唯一的变化是内部调用中的条件。 很简单,对吧? 可以在存储库中查看所有代码。

应用程式


好,那么我们该如何使用这种美丽? 好吧,像这样:

 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"]] 

现在,我们甚至可以直接从此流中馈送Flow以并行化计算。 是的,它仍然是缓慢而悲伤的,但是幸运的是,该任务不是实时的,而是针对可以在夜间运行的分析,它将缓慢地浏览所有组合并在某个位置写下结果。

如果您对Elixir中的AST有疑问-问,我在上面吃了一只狗。

Source: https://habr.com/ru/post/zh-CN477222/


All Articles