Formula dan Kombinator Malas

Perpustakaan Formula


Di fintech, kita sering perlu memverifikasi bahwa kondisi aritmatika sederhana terpenuhi, misalnya, apakah nilai tukar akan lebih besar dari nilai yang diharapkan atau tidak. Kondisi ini sangat sering berubah, dan kami perlu menciptakan beberapa jenis sepeda untuk menambah cek baru dan melakukan yang sudah ada secara real time. Bayangkan bahwa beberapa ribu pelanggan mengharapkan untuk menerima pemberitahuan ketika nilai tukar untuk beberapa pasangan mata uang mencapai rasio dua banding satu. Akan sangat sederhana jika kita hanya bisa membuat kondisi statis:

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

Kami mengizinkan pelanggan untuk menambahkan cek semacam itu secara dinamis. Jadi, kita membutuhkan mekanisme yang kurang lebih andal untuk memeriksa kondisi yang baru saja ditambahkan.

Ya, Code.eval_string/3 entah bagaimana berfungsi, tetapi kompilasi kondisi setiap waktu sebelum benar-benar memeriksa. Jelas, ini adalah pemborosan sumber daya tanpa alasan. Yang diperparah oleh kenyataan bahwa kami menerima dan memproses sekitar 10.000 kursus untuk pasangan mata uang yang berbeda setiap detik.

Jadi kami datang dengan formula yang sudah dikompilasi. Pustaka formulae kecil membuat modul untuk setiap kondisi yang diberikan dan mengkompilasi rumus yang dimasukkan oleh pengguna ke dalam kode - sekali.

Pustaka NB harus digunakan dengan hati-hati, karena nama-nama modul disimpan dalam bentuk atom dan kreasi buta tanpa syarat untuk segala hal yang ingin diperiksa klien dapat mengarah pada serangan DoS atom pada mesin virtual Erlang dengan waktu eksekusi yang kurang lebih panjang. Kami menggunakan langkah maksimum yang diijinkan dari nilai 0.01 , yang memberikan maksimum 200 ribu atom dalam skenario terburuk.

Combinerator Malas


Tetapi tujuan utama dari artikel ini bukan untuk berbicara tentang formula yang sudah dikompilasi. Untuk beberapa kasus batas analisis nilai tukar, kami perlu menghitung permutasi dari daftar yang agak panjang. Tiba-tiba, perpustakaan standar Elixir tidak memberikan solusi turnkey. Yah, saya memutuskan untuk menyalin tanda tangan kombinatorial dari Ruby ( Array#combination sepupu dan sepupu). Sayangnya, ini tidak mudah untuk daftar panjang. Kombinasi terhenti di wilayah tiga puluh elemen dalam daftar, permutasi - bahkan lebih awal.

Nah, diharapkan sudah ada di sini; jadi saya mulai bermain implementasi malas menggunakan Stream. Ternyata, dan itu tidak semudah yang saya kira. Saya datang dengan sesuatu seperti kode di bawah ini

 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) 

Ini berfungsi, tetapi hanya untuk sejumlah kombinasi yang diketahui. Nah, ini mudah diatasi: untuk kasus seperti itu kita memiliki makro, kan?

Dalam kode di atas, tiga pola berbeda dilihat. Cabang yang sukses dari mana kita menjatuhkan daftar. Keluarkan cepat daftar kosong. Dan transformasi aliran dengan indeks. Sepertinya kita bisa mencoba membuat AST untuk hal di atas.

Ini adalah kasus yang jarang terjadi ketika menggunakan Kernel.SpecialForms.quote/2 mungkin hanya akan mempersulit hal-hal, jadi saya mengambil jalan perlawanan paling sedikit: kita akan memahat AST telanjang tua yang baik.

Saya mulai dengan menggunakan quote do: in console pada kode ini, dan, to the point, memeriksa hasilnya. Ya, ada polanya. Baiklah, ayo pergi.

Jadi, Anda harus mulai dengan membuat kerangka kerja umum.

 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 

Sekarang kita harus mulai berpikir dalam hal bukan kode, tetapi AST, untuk melihat bagian template berulang. Ini menyenangkan!

Mari kita mulai dengan makro pembantu untuk menyederhanakan kode:

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

Bagian dalam AST robek dari pandangan umum:

 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 

Semua bagian dalam bersama-sama:

 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 

Dan akhirnya, bungkus luar di sekitar itu semua.

 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 

Semua permutasi dilakukan hampir identik, satu-satunya perubahan adalah kondisi dalam panggilan internal. Itu mudah, bukan? Semua kode dapat dilihat di repositori .

Aplikasi


Bagus, jadi bagaimana kita bisa menggunakan kecantikan ini? Nah, kira-kira seperti ini:

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

Kami sekarang bahkan dapat memberi makan Flow langsung dari aliran ini untuk memparalelkan perhitungan. Ya, itu masih lambat dan sedih, tetapi, untungnya, tugas ini tidak secara real time, tetapi untuk analitik yang dapat dijalankan pada malam hari dan yang perlahan akan melewati semua kombinasi dan menuliskan hasilnya di suatu tempat.

Jika Anda memiliki pertanyaan tentang AST di Elixir - tanyakan, saya memakan seekor anjing di atasnya.

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


All Articles