Indonesian (Bahasa Indonesia) translation by Imam Firmansyah (you can also view the original English article)
Metaprogramming adalah sesuatu yang hebat, tetapi teknik yang sangat sulit, itu berarti sebuah program bisa menganalisis atau bahkan bisa berubah sendiri saat digunakan. Banyak Bahasa sekarang mendukung fitur ini, dan elixir tanpa pengecualian.
Dengan metaprogramming, Anda bisa membuat macro kompleks baru, terdefinisi secara dinamis dan menunda suatu eksekusi kode, yang berarti memperbolehkan Anda untuk menulis kode lebih singkat dan kuat. Ini sudah pasti topik yang berat, tetapi diharapkan setelah membaca artikel ini Anda akan mengerti dasar dari bagaimana cara memulai metaprogramming di Elixir.
Di artikel ini Anda akan mempelajari:
- Apa itu syntax abstrak dan bagaimana kode Elixir bisa diwakili secara rahasia.
- Apa itu
quote
danunquote
function. - Apa itu macros dan bagaimana bekerja dengan itu.
- Bagaimana memasukan values dengan binding.
- Kenapa harus macros higienis.
Sebelum mulai, sekiranya, biarkan saya memberikan sedikit pesan. Ingat kata-kata paman dari spider man's "Dengan kekuatan yang hebat datang dengan tanggung jawab yang besar"? Ini bisa dimasukan kedalam metaprogramming juga karena ini sangat kuat yang bisa membuat Anda memutar dan membelokan kode sesuai keinginan.
Tetap, Anda tidak boleh menyalah gunakan itu, dan Anda harus tetap pada solusi yang sederhana ketika itu bisa dan mungkin. Terlalu banyak metaprogramming membuat kode Anda akan lebih sulit dimengerti dan di tangani, jadi berhati-hatilah.
Abstract Syntax Tree dan Quote
Hal pertama yang harus dimengerti adalah bagaimana kode Elixir sebenarnya ada. Gambaran ini biasanya disebut Abstract Syntax Trees(AST), tetapi petunjuk Elixir yang resmi merekomendasikan mereka menyebut simply quoted expressions.
Sepertinya ekspresi datang dari bentuk tuple dengan tiga elemen, tetapi bagaimana kita membuktikan itu? Baik, ada sebuah fungsi yang disebut quote
yang membalikkan gambaran dari beberapa kode yang diberikan. Pada dasarnya, itu membuat kode menjadi bentuk yang belum terevaluasi. . Sebagai contoh:
quote do 1 + 2 end # => {:+, [context: Elixir, import: Kernel], [1, 2]}
Jadi apa yang terjadi disana? Tuple dikembalikan dari suatu fungsi quote
yang selalu memilki tiga elemen:
- Atom atau tuple lain dengan nilai yang sama. Pada kasus ini, itu adalah atom
:+
, yang berarti kita melakukan sesuatu yang lebih. Ngomong-ngomong, bentuk dari penulisan ini seharusnya familiar jika Anda datang dari dunia ruby. - Daftar dari kata kunci dengan metadata. Sebagai contoh kita melihat modul
Kernel
yang sudah terimport secara otomatis. - Daftar dari argumen atau beberapa atom. Pada kasus ini, ini adalah daftar dengan
1
dan2
.
Gambaran ini mungkin lebih sulit, tentu saja:
quote do Enum.each([1,2,3], &(IO.puts(&1))) end # => {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :each]}, [], # [[1, 2, 3], # {:&, [], # [{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], # [{:&, [], [1]}]}]}]}
Dengan kata lain, beberapa literal mengembalikan nilainya sendiri ketika di quote, secara spesifik:
- atom
- integer
- float
- list
- string
- tuples (tapi hanya dengan dua elemen!)
Pada contoh berikutnya, kita bisa melihat meng-quote beberapa atom dan mengembalikan atom ini kembali:
quote do :hi end # => :hi
Sekarang kita sudah tahu bagaimana kode di gambarkan secara mendalam, mari mulai menuju sesi selanjutnya dan melihat apa itu macros dan kenapa quoted epression itu penting.
Macro
Macro adalah bentuk istimewa seperti function, tetapi satu-satunya yang mengembalikan quoted code. Kode ini kemudian di letakan pada aplikasi, dan eksekusinya di tunda. Apa yang menarik adalah macros juga tidak mengevaluasi parameter yang dikirim ke mereka, mereka menggambarkan sebagai quoted expression juga. Macros bisa digunakan untuk membuat kustomisasi, fungsi yang kompleks digunakan untuk project Anda.
Ingat bahwa, akan tetapi, macros lebih sulit dari fungsi biasa, dan panduan resmi juga menulis bahwa ini harus digunakan pada kesempatan terakhir. Dengan kata lain, jika Anda bisa menggunakan sebuah function, jangan membuat sebuah macro karena akan membuat kode menjadi lebih kompleks dan, secara efektif, lebih sulit untuk ditangani. Tetap, macros ada porsi mereka sendiri, jadi mari kita lihat bagaimana cara membuatnya.
Ini bermula dengan memanggil defmacro
(ini adalah macro itu sendiri):
defmodule MyLib do defmacro test(arg) do arg |> IO.inspect end end
Macro ini secara sederhana menerima beberapa argumen dan menampilkannya.
Juga, ini baik untuk berkata macro bisa di private, sama seperti function. Private macros bisa dipanggil dari modul yang mereka definisi. Untuk mendefinisikan sebuah macro, gunakan defmacrop
.
Sekarang mari membuat modul terpisah yang akan digunakan:
defmodule Main do require MyLib def start! do MyLib.test({1,2,3}) end end Main.start!
Ketika menjalankan kode ini, {:{}, [line: 11], [1, 2, 3]}
akan ditampilkan, yang dimana berarti argument itu memiliki bentuk quote (tidak terevaluasi). Sebelum melakukan proses, namun, biarkan saya membuat sedikit catatan.
Require
Kenapa kita membuat dua modul yang berbeda: satu untuk mendefinisikan sebuah macro dan satu untuk menjalankan suatu kode? Sepertinya kita harus melakukan seperti ini, karena macros di proses sebelum program di eksekusi. Kita juga harus memastikan macro yang terdefinisi ada modul yang tersedia, dan ini dapat dilakukan dengan require
. Fungsi ini, pada dasarnya, memastikan modul yang diberikan ter compile sebelum yang sekarang berjalan.
Anda akan bertanya, kenapa kita tidak menghilangkan main module? Coba lakukan ini:
defmodule MyLib do defmacro test(arg) do arg |> IO.inspect end end MyLib.test({1,2,3}) # => ** (UndefinedFunctionError) function MyLib.test/1 is undefined or private. However there is a macro with the same name and arity. Be sure to require MyLib if you intend to invoke this macro # MyLib.test({1, 2, 3}) # (elixir) lib/code.ex:376: Code.require_file/2
Sayangnya, kita mendapatkan pesan error yang berkata bahwa test tidak dapat ditemukan, walaupun ada macro yang memilki nama yang sama. Ini terjadi karena MyLib
di definisikan pada scope yang sama (dan file yang sama) dimana kita berusaha menggunakan itu. Ini mungkin agak aneh, tetapi untuk sekarang harus diingat bahwa modul yang berbeda harus dibuat untuk menghindari masalah ini
Juga perlu dicatat macros tidak bisa digunakan secara global: pertama Anda harus import atau membutuhkan module tertentu.
Macro dan Ekspresi Quoted
Jadi sekarang kita tahu bagaimana Elixir expression di tampilkan dari dalam dan apa itu macros… Sekarang apa? Baik, sekarang kita dapat memanfaatkan pengetahuan ini dan melihat bagaimana quoted code bisa di evaluasi.
Mari kita kembali pada macros. Ini penting untuk diketahui bahwa akhir ekspresi dari semua macro adalah diharapkan sebagian quoted code yang akan di eksekusi dan di kembalikan secara otomatis ketika macro di panggil. Kita dapat menulis kembali contoh dari sesi sebelumnya dengan memindahkan IO.inspect
ke Main
modul:
defmodule MyLib do defmacro test(arg) do arg end end defmodule Main do require MyLib def start! do MyLib.test({1,2,3}) |> IO.inspect end end Main.start! # => {1, 2, 3}
Lihat apa yang terjadi? Tuple dikembalikan oleh macro yang tidak di memilki quote tetapi di evaluasi! Anda boleh mencoba menambahkan dua integer:
MyLib.test(1 + 2) |> IO.inspect # => 3
Sekali lagi, kode di eksekusi, dan 3
ditampilkan. Kita bisa mencoba menggunakan quote
function secara langsung, dan akhir dari baris tetap bisa di evaluasi:
defmodule MyLib do defmacro test(arg) do arg |> IO.inspect quote do {1,2,3} end end end # ... def start! do MyLib.test(1 + 2) |> IO.inspect # => {:+, [line: 14], [1, 2]} # {1, 2, 3} end
Arg
wtelah di quote (catatan, ngomong-ngomong, kita masih bisa melihat nomor pada baris dimana macro dipanggil), tetapi quoted expression dengan tuple {1,2,3}
telah di evaluasi untuk kita karena ini adalah baris terakhir dari macro.
Kita mungkin tergoda untuk menggunakan arg
dalam ekspresi matematika:
defmacro test(arg) do quote do arg + 1 end end
Tetapi ini akan meningkatkan error yang mengatakan arg
tidak ada. Kenapa begitu? Ini karena arg
biasanya dimasukan ke string yang kita quote. Tapi apa yang harus kita lakukan selain mengevaluasiarg
, memasukan hasil ke string, dan kemudian melakukan quote. Untuk melakukan ini, kita membutuhkan fungsi lain yang disebut unquote
.
Unquoting Kode
unquote
adalah sebuah fungsi yang memasukan hasil dari evaluasi kode ke dalam kode yang akan di berikan quote. Ini mungkin terdengar aneh, tetapi di kenyataan ini lumayan mudah dilakukan. Mari merubah contoh kode sebelumnya:
defmacro test(arg) do quote do unquote(arg) + 1 end end
Sekarang program kita akan mengembalikan 4
, yang dimana itu adalah yang kita inginkan! Yang terjadi adalah kode itu membiarkan unquote
function berjalan hanya ketika quoted code telah di eksekusi, tidak ketika awal di urai.
Mari melihat contoh yang lebih sulit. Seharusmya kita akan membuat sebuah fungsi yang akan menjalakan beberapa ekspresi jika string yang diberikan adalah suatu palindrome. Kita bisa menulis sesuatu seperti ini:
def if_palindrome_f?(str, expr) do if str == String.reverse(str), do: expr end
Akhiran _f
disini berarti fungsi ini akan membuat macro yang sama. Namun, jika kita mencoba untuk menjalankan fungsi ini sekarang, teks tetap akan ditampilkan walaupun string bukanlah sebuah palindrome:
def start! do MyLib.if_palindrome_f?("745", IO.puts("yes")) # => "yes" end
Argumen itu melewati sebuah fungsi yang di evaluasi sebelum fungsi itu benar-benar dipanggil, jadi kita melihat sebuah string "yes"
yang ditampilkan pada layar.ini sudah pasti bukan yang ingin kita capai, jadi mari kita mencoba menggunakan macro:
defmacro if_palindrome?(str, expr) do quote do if(unquote(str) == String.reverse( unquote(str) )) do unquote(expr) end end end # ... MyLib.if_palindrome?("745", IO.puts("yes"))
Disini kita meng-quote kode yang mengandung if
condition dan menggunakan unquote
untuk mengevaluasi nilai dari sebuah argumen ketika macro benar-benar dipanggil. Pada contoh ini, tidak akan ada yang ditampilkan pada layar, yang berarti benar!
Injecting Values Dengan Bindings
Menggunakan unquote
tidak hanya satu-satunya cara untuk memasukan kode ke quoted block. Kita juga bisa menggunakan fitur bernama binding. Sebenarnya, ini secara sederhana sebuah pilihan untuk melanjutkan ke quote
function yang menerima sebuah daftar keyword dengan semua variable yang memiliki unquote maksimal satu kali.
Untuk melakukan binding, gunakan bind_quoted
ke quote
function seperti ini:
quote bind_quoted: [expr: expr] do end
Ini akan berguna ketika Anda menginginkan ekspresi yang menggunakan banyak tempat untuk di evaluasi hanya sekali. Seperti yang di demonstasikan pada contoh ini, kita dapat membuat sebuah macro sederhana yang memiliki output string dua kali dengan penundaan selama dua detik:
defmodule MyLib do defmacro test(arg) do quote bind_quoted: [arg: arg] do arg |> IO.inspect Process.sleep 2000 arg |> IO.inspect end end end
Sekarang, jika Anda memanggil itu untuk melewati waktu pada sistem, dua baris ini memiliki hasil yang sama:
:os.system_time |> MyLib.test # => 1547457831862272 # => 1547457831862272
Ini bukanlah kasus dengan unquote
, karena argument ini akan di evaluasi dua kali dengan keterlambatan yang kecil, jadi hasilnya tidak akan sama:
defmacro test(arg) do quote do unquote(arg) |> IO.inspect Process.sleep(2000) unquote(arg) |> IO.inspect end end # ... def start! do :os.system_time |> MyLib.test # => 1547457934011392 # => 1547457936059392 end
Mengubah Quoted Code
Terkadang, Anda akan mengerti apa bentuk dari quoted code yang sebenarnya ketika kita melakukan debug pada itu, sebagai contoh. Ini bisa di selesaikan dengan menggunakan to_string
function:
defmacro if_palindrome?(str, expr) do quoted = quote do if(unquote(str) == String.reverse( unquote(str) )) do unquote(expr) end end quoted |> Macro.to_string |> IO.inspect quoted end
String yang di cetak akan seperti ini:
"if(\"745\" == String.reverse(\"745\")) do\n IO.puts(\"yes\")\nend"
Kita bisa melihat str
rgumen yang diberikan telah di evaluasi, dan hasilnya adalah telah dimasukan kedalam code. \n
berarti "baris baru".
Juga, kita dapat menambahkan quoted code menggunakan expand_once
dan expand
:
def start! do quoted = quote do MyLib.if_palindrome?("745", IO.puts("yes")) end quoted |> Macro.expand_once(__ENV__) |> IO.inspect end
Yang akan menghasilkan:
{:if, [context: MyLib, import: Kernel], [{:==, [context: MyLib, import: Kernel], ["745", {{:., [], [{:__aliases__, [alias: false, counter: -576460752303423103], [:String]}, :reverse]}, [], ["745"]}]}, [do: {{:., [], [{:__aliases__, [alias: false, counter: -576460752303423103], [:IO]}, :puts]}, [], ["yes"]}]]}
Tentu saja, representasi quote ini bisa kembali lagi menjadi sebuah string:
quoted |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.inspect
Kita akan mendapatkan hasil yang sama dari sebelumnya:
"if(\"745\" == String.reverse(\"745\")) do\n IO.puts(\"yes\")\nend"
Expand
function itu lebih sulit saat dicoba untuk mengekspansi setiap macro yang diberikan pada code:
quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.inspect
Hasilnya akan seperti ini:
"case(\"745\" == String.reverse(\"745\")) do\n x when x in [false, nil] ->\n nil\n _ ->\n IO.puts(\"yes\")\nend"
Kita melihat hasil ini karena if
ini benar sebuah macro yang bergantung pada case
statement, itu akan ikut terekspansi juga.
Sebagai contoh, __ENV__
bentuk spesial dari yang mengembalikan environment information seperti module, file, line, variable pada ruang lingkup saat ini dan imports.
Macros Higienis
Anda mungkin pernah mendengar macros sebenarnya higienis. Apa yang dimaksud adalah dia tidak mengubah setiap variable diluar dari cakupan kita. Untuk membuktikan ini, mari menambahkan contoh variable, coba mengubah nilai dari berbagai tempat, dan kemudian liat hasilnya:
defmacro if_palindrome?(str, expr) do other_var = "if_palindrome?" quoted = quote do other_var = "quoted" if(unquote(str) == String.reverse( unquote(str) )) do unquote(expr) end other_var |> IO.inspect end other_var |> IO.inspect quoted end # ... def start! do other_var = "start!" MyLib.if_palindrome?("745", IO.puts("yes")) other_var |> IO.inspect end
Jadi other_var
memberikan nilai didalam start!
function, didalam sebuah macro, dan didalam quote
. Anda akan melihat hasil seperti ini:
"if_palindrome?" "quoted" "start!"
Ini berarti variable kita independent, dan kita tidak memperkenalkan konflik apapun yang menggunakan nama yang sama dimanapun (Meskipun, tentu saja, ini akan lebih baik untuk menjauhi dari pendekatan semacam itu).
Jika Anda benar menginginkan untuk merubah variabel yang ada diluar didalam sebuah macro, Anda mungkin akan menggunakan var!
seperti ini:
defmacro if_palindrome?(str, expr) do quoted = quote do var!(other_var) = "quoted" if(unquote(str) == String.reverse( unquote(str) )) do unquote(expr) end end quoted end # ... def start! do other_var = "start!" MyLib.if_palindrome?("745", IO.puts("yes")) other_var |> IO.inspect # => "quoted" end
Dengan menggunakan var!
, kita secara efektif berkata bahwa variabel yang diberikan seharusnya tidak higienis. Sangat berhati-hatilah dengan melakukan pendekatan ini, Namun, Anda akan kehilangan jejak dengan apa yang sedang terjadi.
Kesimpulan
Pada artikel ini, kita telah mendiskusikan metaprogramming dasar dengan Bahasa Elixir. Kita telah mengetahui penggunaan dari quote
, unquote
, macros dan bindings ketika melihat beberapa contoh dan kasus. Pada saat ini, Anda sudah siap untuk menerapkan pengetahuan ini pada latihan dan membuat lebih ringkas dan program yang kuat. Ingat, walaupun, itu biasanya lebih baik untuk mempunyai pemahaman kode daripada meringkat kode, jadi jangan terlalu menggunakan metaprogramming pada project Anda.
Jika Anda suka untuk mempelajari lebih lanjut tentang fitur yang telah saya deskripsikan, silahkan untuk membaca pedoman untuk memulai macros, quote dan unquote. Saya sangat berharap artikel ini memberikan Anda sebuah informasi yang menarik tentang metaprogramming di elixir, yang sudah pasti cukup sulit pada awalnya. Bagaimanapun juga, jangan takut untuk bereksperimen dengan tools baru!
Saya berterima kasih untuk telah bersama saya, dan sampai bertemu berikutnya.