公式库
在金融科技领域,我们经常需要验证是否满足简单的算术条件,例如,汇率是否大于预期值。 这些条件经常变化,我们需要发明一种自行车,以便添加新检查并实时执行现有检查。 想象一下,当某些货币对的汇率达到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>>
现在,我们甚至可以直接从此流中馈送
Flow
以并行化计算。 是的,它仍然是缓慢而悲伤的,但是幸运的是,该任务不是实时的,而是针对可以在夜间运行的分析,它将缓慢地浏览所有组合并在某个位置写下结果。
如果您对Elixir中的AST有疑问-问,我在上面吃了一只狗。