Elixir menghubungkan waktu kompilasi

Elixir dilengkapi dengan infrastruktur makro yang canggih, dirancang dengan sangat baik. Dengan tangan ringan Chris McCord, ada undang-undang tidak tertulis di komunitas yang tidak dapat dihindarkan segera disuarakan mengenai makro: "Aturan pertama untuk menggunakan makro adalah bahwa Anda tidak boleh menggunakannya." Kadang-kadang dengan komentar yang tidak jelas diketik dalam font abu-abu pucat dari ukuran keempat: "hanya jika Anda tidak bisa menghindarinya, dan Anda sangat mengerti apa yang akan Anda lakukan dan apa yang Anda riskan." Hal ini disebabkan fakta bahwa makro memiliki akses ke seluruh AST dari modul di mana mereka digunakan, dan, secara umum, mereka dapat mengubah kode yang dihasilkan di luar pengakuan.


Pada prinsipnya, saya setuju bahwa Anda tidak boleh menggunakan makro dalam proses pengenalan dengan bahasa. Sejauh ini, Anda tidak bisa, terbangun pada pukul tiga pagi dengan hangover, menjawab pertanyaan apakah kode ini dieksekusi pada tahap kompilasi, atau dalam runtime. Elixir adalah bahasa yang dikompilasi, dan selama proses kompilasi kode "tingkat atas" dieksekusi, pohon sintaksis sepenuhnya diperluas sampai kita menemukan diri kita dalam situasi di mana tidak ada lagi yang bisa dibuka, dan hasil ini akhirnya dikompilasi dalam BEAM . Ketika kompilator menemukan panggilan makro dalam kode sumber, itu sepenuhnya memaparkan AST untuk itu dan crams bukannya panggilan yang sebenarnya . Mustahil untuk dipahami, hanya bisa diingat.


Tetapi begitu kita merasa bebas dalam sintaks, kita pasti ingin menggunakan kekuatan penuh dari toolkit; di sini tanpa makro - tidak ada tempat. Hal utama adalah jangan menyalahgunakannya. Makro dapat secara dramatis mengurangi (ke nilai negatif) jumlah kode boilerplate yang mungkin diperlukan, dan mereka memberikan cara alami dan sangat nyaman untuk memanipulasi AST . Phoenix , Ecto , dan semua perpustakaan terkenal menggunakan makro sangat banyak.


Hal di atas berlaku untuk semua perpustakaan / paket universal. Dalam pengalaman saya, proyek biasa mungkin tidak diperlukan makro, atau diperlukan dalam area aplikasi yang sangat terbatas. Perpustakaan, sebaliknya, sering terdiri dari makro dalam rasio 80/20 ke kode biasa.


Saya tidak akan mengatur kotak pasir di sini dan memahat muffin makro bagi mereka yang belum tahu apa yang mereka makan; jika menarik untuk mulai belajar dari dasar-dasar, untuk memahami apa itu pada prinsipnya, atau bagaimana dan mengapa mereka digunakan dalam Elixir itu sendiri, yang terbaik adalah segera menutup halaman ini dan membaca buku brilian Metaprogramming Elixir oleh Chris McCord, pencipta Phoenix Framework . Saya hanya ingin menunjukkan beberapa trik untuk membuat ekosistem makro yang ada menjadi lebih baik.




Makro sengaja didokumentasikan dengan buruk. Pisau ini terlalu tajam untuk diiklankan untuk anak-anak.


Ada dua cara untuk menggunakan makro. Yang paling sederhana adalah Anda menginstruksikan kompiler bahwa modul ini akan menggunakan makro dari yang lain menggunakan direktif Kernel.SpecialForms.require/2 , dan memanggil makro itu sendiri setelah itu (untuk makro yang didefinisikan dalam modul yang sama, kebutuhan eksplisit tidak diperlukan). Pada artikel ini kami tertarik dengan cara lain yang lebih rumit. Ketika panggilan kode eksternal use MyLib , diharapkan modul __using__/1 kami MyLib mengimplementasikan __using__/1 makro, yang akan use MyLib oleh kompiler ketika bertemu use MyLib . Gula sintaksis, ya. Konvensi tentang konfigurasi. Kereta melewati Jose tidak lewat tanpa bekas.


Perhatian: jika paragraf di atas membingungkan Anda, dan semua hal di atas terdengar konyol, silakan berhenti makan kaktus ini, dan bacalah buku yang saya sebutkan di atas sebagai ganti catatan kue pendek saya.


__using__/1 mengambil argumen, sehingga pemilik perpustakaan dapat mengizinkan pengguna untuk mengirimkan beberapa parameter ke sana. Ini adalah contoh dari salah satu proyek internal saya yang menggunakan panggilan makro dengan parameter:


 defmodule User do use MyApp.ActiveRecord, repo: MyApp.Repo, roles: ~w|supervisor client subscriber|, preload: ~w|setting companies|a 

Argumen jenis keyword() akan diteruskan ke MyApp.ActiveRecord.__using__/1 , dan di sana saya menggunakannya untuk membuat berbagai pembantu untuk bekerja dengan model ini. ( Catatan: kode ini telah lama diminum karena ActiveRecord kehilangan dalam semua hal panggilan Ecto asli).




Terkadang kami ingin membatasi penggunaan makro untuk subset modul (misalnya, izinkan hanya digunakan dalam struktur). Pemeriksaan eksplisit di dalam __using__/1 tidak akan berfungsi, seperti yang kita inginkan, karena selama kompilasi modul kita tidak memiliki akses ke __ENV__ nya (dan itu akan menjadi - itu masih jauh dari selesai pada saat kompiler menemukan panggilan __using__/1 Ini akan ideal untuk melakukan pemeriksaan ini setelah kompilasi selesai.


Tidak masalah! Ada dua atribut modul yang mengkonfigurasi hal itu. Selamat datang untuk mengunjungi kami, panggilan balik waktu kompilasi yang terhormat.


Berikut adalah kutipan singkat dari dokumentasi.


@after_compile panggilan balik akan dipanggil segera setelah kompilasi modul saat ini.

Menerima modul atau tuple {module, function_name} . Callback itu sendiri harus mengambil dua argumen: lingkungan modul dan bytecode-nya. Ketika hanya modul yang dilewatkan sebagai argumen, diasumsikan bahwa modul ini mengekspor fungsi __after_compile__/2 ada.

Panggilan balik yang terdaftar terlebih dahulu akan dieksekusi terakhir.
 defmodule MyModule do @after_compile __MODULE__ def __after_compile__(env, _bytecode) do IO.inspect env end end 

Saya sangat tidak menyarankan menyuntikkan __after_compile__/2 langsung ke kode yang dihasilkan, karena ini dapat menyebabkan konflik dengan niat pengguna akhir (yang mungkin ingin menggunakan penangan mereka sendiri). Tentukan fungsi di suatu tempat di dalam MyLib.Helpers Anda. MyLib.Helpers atau sesuatu, dan berikan tuple ke @after_compile :


 quote location: :keep do @after_compile({MyLib.Helpers, :after_mymodule_callback}) end 



Callback ini akan dipanggil segera setelah mengkompilasi modul yang sesuai, yang menggunakan perpustakaan kami, dan akan menerima dua parameter: struktur __ENV__ dan kode byte dari modul yang dikompilasi. Yang terakhir ini jarang digunakan oleh manusia biasa; yang pertama menyediakan semua yang kita butuhkan. Berikut ini adalah contoh bagaimana saya melindungi diri dari mencoba memanggil use Iteraptable dari modul yang tidak menerapkan struktur. Bahkan, kode verifikasi hanya memanggil dari __struct__ __struct__ pada modul yang dikompilasi dan delegasi dangkal Elixir hak untuk melempar pengecualian dengan teks yang jelas menjelaskan penyebab masalah:


 def struct_checker(env, _bytecode), do: env.module.__struct__ 

Kode di atas akan mengeluarkan pengecualian jika modul yang dikompilasi bukan struktur. Tentu saja, kode verifikasi bisa jauh lebih rumit, tetapi gagasan utamanya adalah apakah modul yang Anda gunakan mengharapkan sesuatu dari modul yang menggunakannya . Jika demikian, masuk akal untuk tidak melupakan @after_compile dan mengutuk dari sana jika semua persyaratan yang diperlukan tidak terpenuhi. Melempar pengecualian adalah pendekatan yang tepat dalam kasus ini sedikit lebih dari biasanya, karena kode ini dieksekusi pada tahap kompilasi.




Sebuah cerita lucu terhubung dengan @after_callback , yang sepenuhnya menjelaskan mengapa saya menyukai OSS secara umum dan Elixir pada khususnya. Sekitar setahun yang lalu, saya membuat kesalahan dalam copy-paste dan disalin dari suatu tempat @after_callback bukannya @before_callback . Perbedaan di antara mereka mungkin jelas: yang kedua dipanggil sebelum kompilasi , dan dari sana setiap orang dapat mengubah sintaksis pohon di luar pengakuan. Dan saya - oh, bagaimana - saya mengubahnya. Tapi ini tidak mengarah ke hasil apa pun dalam kode yang dikompilasi: itu tidak berubah sama sekali. Setelah tiga cangkir kopi, saya perhatikan salah ketik, diganti after dengan before dan semuanya dimulai; tetapi pertanyaan itu menyiksaku: mengapa kompiler tetap diam, seperti partisan. Ternyata Module.open?/1 mengembalikan true dari callback ini (yang, pada prinsipnya, tidak jauh dari kebenaran - modul masih belum ditutup, akses ke atributnya tidak ditutup, dan banyak perpustakaan menggunakan bug tidak berdokumen ini).


Yah, saya membuat sketsa perbaikan, mengirim permintaan tarik ke kerak bahasa (ke kompiler, jika benar-benar ketat), dan kurang dari sehari kemudian, dia berakhir di master .


Jadi saat itulah saya membutuhkan pengaturan pengguna di IO.inspect/2 , dan dalam beberapa kasus. Apa yang akan terjadi jika saya menemukan ini di Jawa? - Menakutkan membayangkan.




Selamat menikmati makro!

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


All Articles