مكتبة الصيغة
في fintech ، نحتاج غالبًا إلى التحقق من استيفاء الشروط الحسابية البسيطة ، على سبيل المثال ، ما إذا كان سعر الصرف سيكون أكبر من القيمة المتوقعة أم لا. تتغير هذه الظروف في كثير من الأحيان ، ونحن بحاجة إلى اختراع نوع من الدراجات من أجل إضافة اختبارات جديدة وإجراء الاختبارات الحالية في الوقت الحقيقي. تخيل أن عدة آلاف من العملاء يتوقعون تلقي إشعارات عندما يصل سعر الصرف لبعض أزواج العملات إلى اثنين إلى واحد. سيكون الأمر بسيطًا للغاية إذا تمكنا من جعل الظروف ثابتة فقط:
def notify?(rate) when rate > 2.0, do: true def notify?(_), do: false
نحن نسمح للعملاء بإضافة مثل هذه الشيكات بشكل حيوي. لذلك ، نحن بحاجة إلى آلية أكثر أو أقل موثوقية للتحقق من الظروف المضافة للتو.
نعم ،
Code.eval_string/3
بطريقة أو بأخرى ، لكنه يجمع الحالة في كل مرة قبل التحقق الفعلي. من الواضح أن هذا مضيعة للموارد دون سبب. مما يضاعف من حقيقة أننا نتلقى ونعالج حوالي 10000 دورة تدريبية لأزواج العملات المختلفة كل ثانية.
لذلك توصلنا إلى صيغ precompiled. تقوم مكتبة
formulae
الصغيرة بإنشاء وحدة نمطية لكل شرط معين وتجميع الصيغة التي أدخلها المستخدم في التعليمات البرمجية - مرة واحدة.
يجب استخدام مكتبة
NB بحذر ، لأن أسماء الوحدات النمطية يتم تخزينها في شكل ذرات وإنشاءها غير المشروط الأعمى لكل شيء يريد العميل التحقق منه يمكن أن يؤدي إلى هجوم DoS ذري على الجهاز الظاهري Erlang مع وقت تنفيذ طويل أو أكثر. نستخدم الحد الأقصى المسموح به للخطوة بقيمة
0.01
، والذي يعطي بحد أقصى 200 ألف ذرة في أسوأ سيناريو.
Combinators كسول
لكن الغرض الرئيسي من هذا المقال ليس الحديث عن الصيغ المترجمة مسبقًا. بالنسبة لبعض حالات تحليل سعر الصرف على الحدود ، كنا بحاجة إلى حساب التباديل لقائمة طويلة إلى حد ما. فجأة ، لا توفر مكتبة Elixir القياسية حلاً جاهزًا. حسنًا ، قررت نسخ التواقيع التوافقية من روبي (
Array#combination
وأبناء العم). لسوء الحظ ، لم يكن ذلك سهلاً بالنسبة للقوائم الطويلة. المجموعات المتوقفة في منطقة ثلاثين عنصرا في القائمة ، التقليب - حتى قبل ذلك.
حسنا ، من المتوقع أن هنا بالفعل ؛ لذلك بدأت اللعب تطبيق كسول باستخدام تيار. اتضح أنها ليست سهلة كما اعتقدت. لقد توصلت إلى شيء مثل الرمز أدناه
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
مباشرة من هذا التدفق لموازنة الحسابات. نعم ، ستظل بطيئة وحزينة ، لكن لحسن الحظ ، فإن هذه المهمة ليست في الوقت الفعلي ، ولكن بالنسبة للتحليلات التي يمكن تشغيلها في الليل والتي ستستمر ببطء في جميع التوليفات وتدوين النتائج في مكان ما.
إذا كانت لديك أسئلة حول AST في Elixir - اسأل ، أكلت كلبًا عليها.