Crates and modules

Setiap kali kita menuliskan code di Rust, kita menuliskannya di dalam crate. crate adalah sebuah file, atau banyak file, yang berjalan bersama code kita. Di dalam file yang Anda tulis, Anda juga bisa membuat mod. mod adalah wadah untuk function, struct, dll. Dan ia digunakan untuk beberapa alasan:

  • Menuliskan code: ia membantu Anda untuk memikirkan tentang gambara besar dari code yang ingin Anda buat. Ini menjadi sangat penting sebagaimana code Anda semakin membesar dan terus membesar.
  • Membaca code: orang lain jadi bisa memahami code yang kita tuliskan dengan sangat mudah. Contohnya, nama std::collections::HashMap memberitahukan kita bahwa ia merupakan bagian dari std dan di dalam sebuah module bernama collections. Ini memberikan Anda petunjuk bahwa mungkin ada lebih banyak type collection di dalam collections yang bisa Anda coba.
  • Privasi: semua dimulai dengan private. Hal ini memungkinkan Anda untuk mencegah pengguna menggunakan function secara langsung.

Untuk membuat mod, cukup tuliskan mod dan mulai code block dengan menggunakan {}. Kita akan membuat sebuah mod bernama print_things yang memiliki beberapa function yang berkaitan dengan cetak-mencetak.

mod print_things {
    use std::fmt::Display;

    fn prints_one_thing<T: Display>(input: T) { // Print apapun yang mengimplementasikan Display
        println!("{}", input)
    }
}

fn main() {}

Anda bisa melihat bahwa kita menuliskan use std::fmt::Display; di dalam print_things, karena ia adalah tempat yang terpisah. Jika Anda menuliskan use std::fmt::Display; di dalam main(), hal itu tidaklah berguna. Juga, sekarang ini kita tidak bisa memanggil function prints_one_thing dari function main(). Tanpa menggunakan keyword pub di depan fn ia akan tetap bersifat private. Mari kita coba memanggilnya tanpa pub. Ini adalah salah satu cara untuk menuliskannya:

// 🚧
fn main() {
    crate::print_things::prints_one_thing(6);
}

crate artinya "di dalam project ini", atau dengan bahasa yang lebih sederhana "di dalam file ini". Di dalam crate tersebut terdapat mod bernama print_things, dan kemudian ada function prints_one_thing(). Anda bisa menuliskannya seperti itu setiap saat, atau Anda bisa menuliskannnya menggunakan use mengimportnya. Sekarang kita bisa melihat error yang mengatakan bahwa ia bersifat private:

// ⚠️
mod print_things {
    use std::fmt::Display;

    fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::print_things::prints_one_thing;

    prints_one_thing(6);
    prints_one_thing("Trying to print a string...".to_string());
}

Berikut adalah pesan errornya:

error[E0603]: function `prints_one_thing` is private
  --> src\main.rs:10:30
   |
10 |     use crate::print_things::prints_one_thing;
   |                              ^^^^^^^^^^^^^^^^ private function
   |
note: the function `prints_one_thing` is defined here
  --> src\main.rs:4:5
   |
4  |     fn prints_one_thing<T: Display>(input: T) {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sangatlah mudah untuk memahami bahwa function prints_one_thing adalah private. Pesan error tersebut juga menuliskan src\main.rs:4:5 yaitu letak dimana functionnya berada. Ini cukup membantu karena Anda bisa menulis mod tidak hanya dalam satu file, tetapi bisa ditulisakn di banyak file juga.

Sekarang kita menuliskan pub fn menggantikan fn, dan codenya bekerja.

mod print_things {
    use std::fmt::Display;

    pub fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::print_things::prints_one_thing;

    prints_one_thing(6);
    prints_one_thing("Trying to print a string...".to_string());
}

Hasil printnya adalah:

6
Trying to print a string...

Bagaimana pub pada sebuah struct, enum, trait, atau module? pub bekerja seperti ini untuk mereka masing-masing:

  • pub pada sebuah struct: ia membbuat structnya menjadi public, namun item-item di dalamnya tidaklah public. Untuk membuat itemnya public, Anda perlu menuliskan pub pada setiap item tersebut.
  • pub pada sebuah enum atau trait: semuanya menjadi public. Ini sangatlah masuk akal, karena trait adalah tentang memberikan perilaku yang sama pada sesuatu. Dan enum adalah tentang memilih salah satu dari banyaknya item, dan Anda perlu melihat semua pilihan tersebut untuk memilihnya.
  • pub pada sebuah module: top level module semestinya akan menjadi pub karena jika ia bukanlah pub maka tidak ada yang bisa menyentuh apapun yang berada di dalamnya. Tapi module yang berada di dalam module memerlukan pub untuk menjadi public.

Jadi, mari kita tambahkan sebuah struct bernama Billy di dalam print_things. Struct ini hampir semua isinya akan menjadi bersifat public, namun tidak sepenuhnya. Struct sendiri adalah public, sehingga tentu saja kita nantinya akan menuliskan pub struct Billy. Di dalam strust tersebut terdapat field name dan times_to_print. name tidaklah public, karena kita hanya menginginkan user membuat struct dengan nama "Billy".to_string(). Tapi pengguna dapat memilih berapa kali untuk mencetak, sehingga times_to_print akan menjadi publik. Codenya terlihat seperti ini:

mod print_things {
    use std::fmt::{Display, Debug};

    #[derive(Debug)]
    pub struct Billy { // Billy adalah public
        name: String, // name adalah private.
        pub times_to_print: u32,
    }

    impl Billy {
        pub fn new(times_to_print: u32) -> Self { // Ini artinya user perlu untuk menggunakan new untuk membuat Billy. User hanya bisa mengubah angka dari times_to_print
            Self {
                name: "Billy".to_string(), // Kita pilihkan namanya - user tidak bisa memilihkannya
                times_to_print,
            }
        }

        pub fn print_billy(&self) { // function ini akan mencetak Billy
            for _ in 0..self.times_to_print {
                println!("{:?}", self.name);
            }
        }
    }

    pub fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::print_things::*; // Sekarang kita menggunakan *. Ini mengimport semua dari print_things

    let my_billy = Billy::new(3);
    my_billy.print_billy();
}

This will print:

"Billy"
"Billy"
"Billy"

Ah ya, tanda * yang digunakan untuk mengimport semuanya disebut sebagai "glob operator". Glob artinya "global", sehingga itu berarti seluruhnya/semuanya.

Di dalam sebuah mod Anda bisa membuat mod yang lain. Child mod (mod yang berada di dalam mod) selalu bisa menggunakan apapun yang ada di dalam parent mod. Anda bisa melihat ini pada contoh selanjutnya dimana kita memiliki mod city di dalam mod province di dalam mod country.

Anda bisa membayangkan strukturnya seperti ini: Meskipun Anda berada di sebuah negara, itu bukan berarti Anda berada di sebuah provinsinya. Dan bahkan jika Anda berada di sebuah provinsi, bukan berarti Anda di dalam kota. Tapi jika Anda berada di dalam kota, maka sudah dipastikan bahwa Anda berada di dalam suatu provinsi dan berada di dalam suatu negara.

mod country { // mod utama tidak memerlukan pub
    fn print_country(country: &str) { // Catatan: function ini tidaklah public
        println!("We are in the country of {}", country);
    }
    pub mod province { // buat mod ini menjadi mod public

        fn print_province(province: &str) { // Catatan: function ini bukanlah public
            println!("in the province of {}", province);
        }

        pub mod city { // buat mod ini menjadi mod public
            pub fn print_city(country: &str, province: &str, city: &str) {  // function ini bersifat public
                crate::country::print_country(country);
                crate::country::province::print_province(province);
                println!("in the city of {}", city);
            }
        }
    }
}

fn main() {
    crate::country::province::city::print_city("Canada", "New Brunswick", "Moncton");
}

Bagian menariknya adalah bahwa print_city bisa mengakses print_province dan print_country. Itu dikarenakan mod city berada di dalam mod-mod lainnya. Ia tidak memerlukan pub di depan print_province untuk menggunakannya. Dan ini sangatlah masuk akal: city tidaklah perlu melakukan apapun untuk berada di dalam province dan di dalam country.

Anda juga mungkin menyadari bahwa crate::country::province::print_province(province); sangatlah panjang. Di saat kita berada di dalam sebuah module, kita bisa menggunakan super untuk membawa item yang berada di level atas. Sebenarnya kata super sendiri artinya adalah "atas", seperti pada kata "superior" (peringkat atas). Pada contoh kita ini, kita hanya menggunakan functionnya sekali saja. Namun jika Anda menggunakannya lebih dari sekali, adalah ide yang bagus untuk mengimportnya. Dan tentu saja ini merupakan hal yang baik pula jika membuat codenya menjadi lebih mudah untuk dibaca, meskipun Anda hanya menggunakan fungsinya hanya satu kali saja. Code yang ada di bawah ini hampir sama, tapi sedikit lebih mudah dibaca:

mod country {
    fn print_country(country: &str) {
        println!("We are in the country of {}", country);
    }
    pub mod province {
        fn print_province(province: &str) {
            println!("in the province of {}", province);
        }

        pub mod city {
            use super::super::*; // gunakan semua yang berada di "above above": yang mana itu artinya adalah mod country
            use super::*;        // gunakan semua yang berada di "above": yang artinya itu adalah mod province

            pub fn print_city(country: &str, province: &str, city: &str) {
                print_country(country);
                print_province(province);
                println!("in the city of {}", city);
            }
        }
    }
}

fn main() {
    use crate::country::province::city::print_city; // bawa functionnya ke sini

    print_city("Canada", "New Brunswick", "Moncton");
    print_city("Korea", "Gyeonggi-do", "Gwangju"); // Dengan cara seperti ini, kita tidak perlu menuliskannya secara panjang jika ingin menggunakannya lagi
}