Writing macros

Membuat macro bisa menjadi hal yang rumit. Anda hampir tidak perlu untuk membuatnya, namun terkadang Anda mungkin ingin membuatnya karena hal ini menyenangkan meskipun cukup menantang. Membuat macro sangatlah menarik karena bisa dikatakan bahwa membuat macro ini merupakan bahasa yang hampir berbeda. Untuk membuatnya, Anda sebenarnya perlu menggunakan macro lain yang bernama macro_rules!. Kemudian Anda menambahkan nama macro Anda dan block {}. Di dalamnya ada semacam statement match.

Contoh di bawah ini hanya memerlukan (), kemudian hanya me-return 6:

macro_rules! give_six {
    () => {
        6
    };
}

fn main() {
    let six = give_six!();
    println!("{}", six);
}

Tapi ia sama sekali bukan statement match, karena macro sebenarnya tidak meng-compile apapun. Ia mhanya mengambil input dan memberikan output. Kemudian compiler memeriksa untuk melihat apakah macro rulenya masuk akal. Itulah sebabnya mengapa macro seperti "code yang menuliskan code". Anda akan mengingat bahwa sebuah statement match perlu untuk me-return type yang sama, sehingga code di bawah ini tidak akan berjalan:

fn main() {
// ⚠️
    let my_number = 10;
    match my_number {
        10 => println!("You got a ten"),
        _ => 10,
    }
}

Compiler akan memberikan pesan teguran bahwa Anda ingin me-return () di satu arm, dan me-return i32 di arm yang lainnya.

error[E0308]: `match` arms have incompatible types
 --> src\main.rs:5:14
  |
3 | /     match my_number {
4 | |         10 => println!("You got a ten"),
  | |               ------------------------- this is found to be of type `()`
5 | |         _ => 10,
  | |              ^^ expected `()`, found integer
6 | |     }
  | |_____- `match` arms have incompatible types

Namun macro tidak peduli tentang itu, karena ia hanya memberikan output. Ia bukanlah compiler - ia hanyalah code yang dilandasi oleh code yang lain. Sehingga Anda bisa melakukan hal seperti ini:

macro_rules! six_or_print {
    (6) => {
        6
    };
    () => {
        println!("You didn't give me 6.");
    };
}

fn main() {
    let my_number = six_or_print!(6);
    six_or_print!();
}

Semuanya berjalan normal, dan hasil cetaknya adalah You didn't give me 6.. Anda juga bisa melihat bahwa itu bukanlah arm yang ada pada match karena disitu tidak ada case _. Kita hanya bisa memberikannya (6), atau (). Selain daripada itu akan membuat error. Dan angka 6 yang kita berikan itu pun sebenarnya bukanlah i32, ia hanyalah inputan 6. Anda sebenarnya bisa mengatur apapun sebagai input untuk macro, karena ia hanya melihat input untuk melihat apa yang didapatkannya. Contohnya:

macro_rules! might_print {
    (THis is strange input 하하はは哈哈 but it still works) => {
        println!("You guessed the secret message!")
    };
    () => {
        println!("You didn't guess it");
    };
}

fn main() {
    might_print!(THis is strange input 하하はは哈哈 but it still works);
    might_print!();
}

Jadinya, macro aneh yang kita buat ini hanya memberikan respond pada dua hal: () dan (THis is strange input 하하はは哈哈 but it still works). Tidak ada selain itu. Hasil cetaknya adalah:

You guessed the secret message!
You didn't guess it

Jadi, macro itu sendiri tepatnya bukanlah syntax yang umumnya ada pada Rust. Namun macro juga bisa memahami type yang berbeda dari input yang Anda berikan. Lihatlah contoh ini:

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {}", $input);
    }
}

fn main() {
    might_print!(6);
}

Ia akan mencetak You gave me: 6. Bagian $input:expr adalah bagian yang penting. Ini berarti "untuk sebuah expression, berikan ia nama variabel $input". Di dalam macro, variabel dimulai dengan $. Di dalam macro ini, jika Anda memberikan satu expression, ia akan mencetaknya. Mari kita coba lagi:

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {:?}", $input); // Sekarang kita menggunakan {:?} karena kita akan memberikannya jenis expression yang berbeda
    }
}

fn main() {
    might_print!(()); // berikan ia ()
    might_print!(6); // berikan ia 6
    might_print!(vec![8, 9, 7, 10]); // berikan ia vec
}

Hasil cetaknya adalah:

You gave me: ()
You gave me: 6
You gave me: [8, 9, 7, 10]

Perhatikan juga bahwa kita menuliskan {:?}, namun ia tidak memeriksa apakah &input mengimplementasikan Debug. Ia hanya akan menuliskan code dan mencoba membuatnya ter-compile, dan jika tidak maka ia akan memberikan error.

Jadi apa saja yang bisa dilihat oleh macro selain expr? Mereka adalah: block | expr | ident | item | lifetime | literal | meta | pat | path | stmt | tt | ty | vis. Ini adalah bagian yang rumit. Anda bisa melihat apa arti dari masing-masing macro attribute tersebut di sini, yang mana laman tersebut menjelaskan:

item: an Item
block: a BlockExpression
stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
pat: a Pattern
expr: an Expression
ty: a Type
ident: an IDENTIFIER_OR_KEYWORD
path: a TypePath style path
tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
meta: an Attr, the contents of an attribute
lifetime: a LIFETIME_TOKEN
vis: a possibly empty Visibility qualifier
literal: matches -?LiteralExpression

Ada situs bagus lainnya yang bernama cheats.rs yang menjelaskan semua macro attribute tersebut. Anda bisa membacanya penjelasannya di sini dan disana ada contoh untuk masing-masing macro attribute yang disebutkan itu.

Namun, untuk kebanyakan macro Anda biasanya akan menggunakan expr, ident, dan tt. ident berarti adalah identifier dan ia berguna untuk nama variabel atau nama function. tt adalah token tree dan semacamnya yang berarti itu adalah semua jenis inputan. Mari kita coba buat macro sederhana dengan kedua macro attribute tersebut.

macro_rules! check {
    ($input1:ident, $input2:expr) => {
        println!(
            "Is {:?} equal to {:?}? {:?}",
            $input1,
            $input2,
            $input1 == $input2
        );
    };
}

fn main() {
    let x = 6;
    let my_vec = vec![7, 8, 9];
    check!(x, 6);
    check!(my_vec, vec![7, 8, 9]);
    check!(x, 10);
}

Jadi, macro di atas akan mengambil satu ident (seperti nama variabel) dan sebuah expression, dan melihat apakah ident dan expr tersebut sama. Hasil cetaknya adalah:

Is 6 equal to 6? true
Is [7, 8, 9] equal to [7, 8, 9]? true
Is 6 equal to 10? false

Dan ini adalah satu macro yang mengambil tt dan mencetaknya. Macro tersebut akan menggunakan macro lainnya yang bernama stringify! untuk membuatnya menjadi string terlebih dahulu.

macro_rules! print_anything {
    ($input:tt) => {
        let output = stringify!($input);
        println!("{}", output);
    };
}

fn main() {
    print_anything!(ththdoetd);
    print_anything!(87575oehq75onth);
}

Hasil cetaknya adalah:

ththdoetd
87575oehq75onth

Tetapi ia tidak akan mencetak apapun apabila kita memberikan sesuatu dengan spasi, koma, dll. Ia akan mengira bahwa kita memberikannya lebih dari satu item atau informasi tambahan, sehingga ia akan menjadi bingung.

Di sinilah macro mulai menjadi sulit untuk dibuat.

Untuk memberi macro lebih dari satu item, kita perlu menggunakan syntax yang berbeda. Alih-alih menggunakan $input, kita akan menggunakan $($input1),*. Ini berarti nol, satu atau lebih dari satu (inilah apa yang dimaksud dengan *), dipisahkan dengan koma. Jika Anda menginginkan satu atau lebih, gunakan + alih-alih menggunakan *.

Sekarang macro kita menjadi seperti ini:

macro_rules! print_anything {
    ($($input1:tt),*) => {
        let output = stringify!($($input1),*);
        println!("{}", output);
    };
}


fn main() {
    print_anything!(ththdoetd, rcofe);
    print_anything!();
    print_anything!(87575oehq75onth, ntohe, 987987o, 097);
}

Sehingga ia akan mengambil apapun token tree yang dipisahkan dengan koma, dan menggunakan stringify! untuk membuatnya menjadi string, kemudian mencetaknya. Hasilnya adalah sebagai berikut:

ththdoetd, rcofe

87575oehq75onth, ntohe, 987987o, 097

Jika kita menggunakan + menggantikan *, ia akan memberikan error, karena terkadang kita tidak memberikan input. Sehingga * adalah pilihan yang lebih aman.

Jadi, sekarang kita bisa mulai melihat power dari macro. Pada contoh kali ini, kita sebenarnya bisa membuat function kita sendiri:

macro_rules! make_a_function {
    ($name:ident, $($input:tt),*) => { // Pertama, Anda berikan ia satu nama untuk function tersebut, lalu kemudian memeriksa yang lainnya
        fn $name() {
            let output = stringify!($($input),*); // Ia membuat segala sesuatunya menjadi string
            println!("{}", output);
        }
    };
}


fn main() {
    make_a_function!(print_it, 5, 5, 6, I); // Kita ingin membuat function bernama print_it() yang mencetak apapun yang kita berikan
    print_it();
    make_a_function!(say_its_nice, this, is, really, nice); // Yang dilakukan pada bagian ini juga sama, namun kita mengubah nama functionnya
    say_its_nice();
}

Hasil cetaknya adalah:

5, 5, 6, I
this, is, really, nice

Jadi sekarang kita bisa mulai memahami macro lainnya. Anda bisa melihat bahwa beberapa macro yang pernah kita gunakan ternyata sangatlah sederhana. Salah satu contohnya adalah write! yang biasa kita gunakan untuk menulis ke file:


#![allow(unused)]
fn main() {
macro_rules! write {
    ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*)))
}
}

Jadi untuk menggunakannya, Anda perlu memasukkan ini:

  • sebuah expression (expr) yang mengambil nama variabel $dst.
  • apapun yang ada setelahnya. Jika disitu tertulis $arg:tt maka ia hanya bisa mengambil satu argument, tapi karena disitu tertulis $($arg:tt)* ia akan mengambil nol, satu, atau banyak argument.

Kemudian ia mengambil $dst dan menggunakan method write_fmt pada $dst tersebut. Di dalamnya, ia menggunakan macro lainnya yang bernama format_args! yang mengambil semua $($arg)*, atau semua argument yang kita masukkan.

Sekarang saatnya kita melihat isi dari macro todo!. Macro ini digunakan ketika Anda menginginkan programnya tercompile namun beberapa bagian codenya belum dituliskan. Berikut isi dari macro tersebut:


#![allow(unused)]
fn main() {
macro_rules! todo {
    () => (panic!("not yet implemented"));
    ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+)));
}
}

Macro ini memiliki dua opsi: Anda bisa memasukkan (), atau beberapa token tree (tt).

  • Jika Anda memasukkan (), ia akan menggunakan panic! dengan sebuah pesan. Jadi sebenarnya Anda bisa menulis panic!("not yet implemented") untuk menggantikan todo! dan ia akan melakukan hal yang sama.
  • Jika Anda memasukkan beberapa argument, ia akan mencoba untuk mencetaknya. Anda bisa melihat hal yang sama di dalam macro format_args! macro, yang mana bekerja seperti println!.

Jadi jika Anda menuliskan ini, ia pun juga akan berjalan:

fn not_done() {
    let time = 8;
    let reason = "lack of time";
    todo!("Not done yet because of {}. Check back in {} hours", reason, time);
}

fn main() {
    not_done();
}

Hasilnya adalah:

thread 'main' panicked at 'not yet implemented: Not done yet because of lack of time. Check back in 8 hours', src/main.rs:4:5

Di dalam sebuah macro, Anda bahkan bisa memanggil macro yang sama. Seperti ini contohnya:

macro_rules! my_macro {
    () => {
        println!("Let's print this.");
    };
    ($input:expr) => {
        my_macro!();
    };
    ($($input:expr),*) => {
        my_macro!();
    }
}

fn main() {
    my_macro!(vec![8, 9, 0]);
    my_macro!(toheteh);
    my_macro!(8, 7, 0, 10);
    my_macro!();
}

Macro ini mengambil (), atau satu expression, atau banyak expression. Tapi ia akan mengabaikan semua expression yang diberikan, tidak peduli apapun yang Anda masukkan, dan kita hanya bisa memanggil my_macro! dengan (). Sehingga outputnya adalah Let's print this yang dicetak sebanyak empat kali.

Anda bisa melihat hal yang sama pada macro dbg!, yang mana ia memanggil dirinya sendiri.


#![allow(unused)]
fn main() {
macro_rules! dbg {
    () => {
        $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); //$crate artinya adalah crate yang berada di dalamnya.
    };
    ($val:expr) => {
        // Penggunaan `match` di sini memanglah disengaja karena ia akan memengaruhi lifetime
        // https://stackoverflow.com/a/48732525/1063961
        match $val {
            tmp => {
                $crate::eprintln!("[{}:{}] {} = {:#?}",
                    $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);
                tmp
            }
        }
    };
    // Koma yang mengikuti sebuah argument (koma yang ditulis setelah ditulisnya satu argument, tanpa ada argument lanjutan) akan diabaikan
    ($val:expr,) => { $crate::dbg!($val) };
    ($($val:expr),+ $(,)?) => {
        ($($crate::dbg!($val)),+,)
    };
}
}

(eprintln! sama seperti println!. Yang membedakannya adalah ia akan mencetak ke io::stderr, bukan mencetak ke io::stdout seperti yang dilakukan oleh println!. Ada juga eprint! yang tidak menambahkan baris baru)

Jadinya, kita akan mencoba macro tersebut.

fn main() {
    dbg!();
}

Macro tersebut cocok dengan arm yang pertama, sehingga ia akan mencetak nama file dan nomor line dengan menggunakan macro file! dan line!. Hasil cetaknya adalah [src/main.rs:2].

Akan kita coba dengan vec:

fn main() {
    dbg!(vec![8, 9, 10]);
}

Ini cocok dengan arm yang selanjutnya (arm kedua), karena ia hanya memiliki satu expression. Ia akan memanggil input tmp dan menggunakan code: $crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);. Sehingga ia akan mencetak dengan macro file! dan line!, kemudian membuat $val menjadi String, dan juga pretty print {:#?} untuk tmp. Sehingga inputan vec kita itu akan memberi output seperti ini:

[src/main.rs:2] vec![8, 9, 10] = [
    8,
    9,
    10,
]

Dan selebihnya, ia hanya memanggil dbg! pada dirinya sendiri meskipun Anda memasukkan koma tambahan.

Sebagaimana yang bisa kita lihat, macro sangatlah rumit! Biasanya, kita hanya ingin menggunakan macro yang melakukan sesuatu secara otomatis yang mana tidak bisa dilakukan oleh function sederhana. Cara terbaik untuk mempelajari macro adalah melihat pada contoh macro yang lainnya. Tidak banyak orang yang bisa menulis macro dengan cepat tanpa mendapatkan masalah apapun. Jadi jangan berpikir bahwa Anda perlu mengetahui semua tentang macro untuk mengetahui cara membuat program di Rust. Namun jika Anda membaca macro lainnya yang sudah ada, dan mencoba mengubahnya sedikit-sedikit, Anda bisa dengan mudah meminjam "kekuatan" dari macro ini. Dan kemudian Anda mungkin mulai merasa nyaman untuk menulis macro Anda sendiri.

Part 2 - Rust on your computer

Anda bisa melihat bahwa kita bisa mempelajari hampir semua yang ada di Rust hanya dengan menggunakan Playground. Tapi jika Anda mempelajari semuanya sejauh ini, mungkin saja Anda menginginkan Rust di komputer Anda sekarang. Selalu ada hal-hal yang tidak bisa Anda lakukan di Playground, misalnya menggunakan file atau code yang memiliki lebih dari satu file. Beberapa hal lain yang membuat Anda membutuhkan Rust di komputer Anda adalah untuk mengambil inputan dari user dan juga flag. Namun hal terpenting yang bisa dilakukan oleh Rust yang terinstall di komputer adalah Anda bisa menggunakan crate. Kita telah mempelajari tentang crate, namun di Playground kita hanya bisa menggunakan crate-crate yang paling populer saja. Jadi dengan Rust yang telah terinstall di komputer kita bisa menggunakan crate apapun untuk program yang kita buat.