Using files

Sekarang setelah kita menggunakan Rust di komputer, kita bisa mulai melakukan sesuatu dengan file. Anda akan melihat bahwa sekarang kita akan mulai melihat lebih banyak Result di dalam code kita. Itu dikarenakan saat kita mulai bekerja dengan file dan hal semacamnya, besar kemungkinan kita melakukan kesalahan. Bisa saja mungkin filenya tidak ada di sana (tidak bisa mengaksesnya), atau bisa jadi juga mungkin komputer kita tidak dapat membacanya.

Anda mungkin masih ingat bahwa jika Anda menggunakan operator ?, ia akan mengmbalikan Result pada function tempat ia berada. Jika Anda tidak bisa mengingat error typenya, Anda bisa mengosongkannya (dengan ()) dan biarkan compiler yang memberitahukannya kepada Anda. Mari kita coda dengan sebuah function yang mencoba untuk membuat sebuah angkan menggunakan method .parse().

// ⚠️
fn give_number(input: &str) -> Result<i32, ()> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

Compiler memberi tahu kita secara tepat tentang apa yang harus kita lakukan:

error[E0308]: mismatched types
 --> src\main.rs:4:5
  |
3 | fn give_number(input: &str) -> Result<i32, ()> {
  |                                --------------- expected `std::result::Result<i32, ()>` because of return type
4 |     input.parse::<i32>()
  |     ^^^^^^^^^^^^^^^^^^^^ expected `()`, found struct `std::num::ParseIntError`
  |
  = note: expected enum `std::result::Result<_, ()>`
             found enum `std::result::Result<_, std::num::ParseIntError>`

Mantap! Jadi kita cukup mengubah returnnya menjadi apa yang compiler katakan:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

Sekarang programnya berjalan!

Ok(88)
Ok(5)

Jadi sekarang kita ingin menggunakan ? agar langsung memberikan valuenya jika programnya berjalan, dan memberikan error jika programnya tidak bisa dijalankan. Tapi bagaimana caranya melakukan hal tersebut di dalam fn main()? Jika kita mencoba untuk menggunakan ? di dalam main(), maka ia tidak akan berfungsi.

// ⚠️
use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
}

Compiler akan memunculkan ini:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
  --> src\main.rs:8:22
   |
7  | / fn main() {
8  | |     println!("{:?}", give_number("88")?);
   | |                      ^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
9  | |     println!("{:?}", give_number("5")?);
10 | | }
   | |_- this function should return `Result` or `Option` to accept `?`

Tapi sebenarnya main() bisa mengembalikan Result, sama seperti function lainnya. Jika function kita bekerja, kita tidak ingin me-return apapun (main() tidak memberikan apapun). Dan jika ia tidak bekerja, ia akan mengembalikan error yang sama. Sehingga kita bisa menuliskan codenya seperti ini:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() -> Result<(), ParseIntError> {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
    Ok(())
}

Jangan lupa Ok(()) pada bagian akhir: ini sangatlah umum di Rust. Itu artinya adalah Ok yang di dalamnya ada (), yang mana itu adalah merupakan value kembaliannya. Beginilah outputnya:

88
5

Ini memanglah tidak terlalu berguna di saat kita menggunakan .parse(), namun ia akan berguna ketika kita melakukan sesuatu yang berkaitan dengan file. Ini dikarenakan operator ? juga mengubah error typenya. Inilah informasi yang bisa dilihat pada laman tentang operator ? yang ditulis dengan simple English:

If you get an `Err`, it will get the inner error. Then `?` does a conversion using `From`. With that it can change specialized errors to more general ones. The error it gets is then returned.

Artinya, "Jika Anda mendapatkan Err, ia akan mendapatkan inner error. Kemudian ? melakukan konversi menggunakan From. Dengan itu ia bisa mengubah error yang spesifik menjadi error yang umum. Error yang didapatkan tersebut kemudian dikembalikan."

Juga, Rust memiliki type Result di saat menggunakan File atau hal semacamnya. Ia biasa disebut std::io::Result, dan ini adalah apa yang biasanya Anda lihat di dalam main() saat Anda menggunakan ? untuk membuka dan melakukan sesuatu terhadap file. Itu sebenarnya adalah type alias. Ia terlihat seperti berikut:

type Result<T> = Result<T, Error>;

Jadi ia sebenarnya adalah Result<T, Error>, namun kita hanya perlu menuliskan Result<T>.

Sekarang mari kita coba mengerjakan sesuatu dengan file untuk pertama kalinya. std::fs adalah tempat dimana method-method yang berguna untuk bekerja dengan file berada, dan dengan std::io::Write Anda bisa menuliskan sesuatu ke dalam file tersebut. Dengan std::io::Write kita bisa menggunakan method .write_all() untuk menuliskan sesuatu ke dalam file.

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = fs::File::create("myfilename.txt")?; // Buat sebuah file dengan nama ini.
                                                        // PERINGATAN! Jika Anda sudah memiliki file dengan nama tersebut,
                                                        // ia akan menghapus apapun yang ada di dalamnya.
    file.write_all(b"Let's put this in the file")?;     // Jangan lupa dengan b yang ditulis di depan ". Itu karena file akan mengambil bytenya.
    Ok(())
}

Kemudian jika Anda klik pada file baru tersebut (myfilename.txt), di dalamnya akan ada tulisan Let's put this in the file.

Sebenarnya kita tidak perlu melakukannya dengan menuliskannya dalam 2 baris seperti itu, karena kita menggunakan operator ?. Ia akan pass hasil yang kita inginkan jika ia bekerja, seperti saat kita menggunakan banyak metode pada iterator. Di sinilah kita mendapatkan "kenyamanan" saat menggunakan operator ?.

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::File::create("myfilename.txt")?.write_all(b"Let's put this in the file")?;
    Ok(())
}

Jadi, bahasa mudahnya adalah "Tolong coba buatkan sebuah file dan periksa apakah kita berhasil membuatnya. Jika ya, kemudian gunakan .write_all() untuk menulis sesuatu di dalamnya dan kemudian periksa apakah kita berhasil menuliskannya."

Dan sebenarnya, ada juga function yang melakukan keduanya secara bersamaan (membuat file sekaligus menuliskan sesuatu di dalamnya). Ia adalah std::fs::write. Di dalamnya, Anda memberikan nama file yang Anda inginkan, dan juga isi/tulisan yang ingin Anda masukkan ke dalamnya. Lagi-lagi, berhati-hatilah! Ia akan menghapus apapun yang ada di dalam file tersebut jika sebelumnya file tersebut sudah ada. Juga, method ini memungkinkan kita menuliskan &str tanpa b di bagian depannya, karena beginilah signaturenya:


#![allow(unused)]
fn main() {
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
}

AsRef<[u8]> adalah alasan mengapa Anda bisa menuliskannya tanpa menuliskan b di depannya.

Penggunaannya pun sangat sederhana:

use std::fs;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    Ok(())
}

Jadi, itu merupakan file yang akan kita gunakan. Isinya adalah percakapan antara tokoh fiktif bernama Calvin dan juga ayahnya, yang menjawab pertanyaan anaknya dengan tidak serius. Dengan cara ini, kita bisa membuat sebuah file untuk digunakan setiap saat.

Membuka file sama mudahnya seperti membuat file. Anda cukup menggunakan open(). Setelah itu (jika filenya ditemukan), Anda bisa melakukan sesuatu seperti read_to_string(). Untuk melakukan itu Anda bisa membuat sebuah String yang mutable dan membaca filenya di dalam situ. Codenya menjadi seperti ini:

use std::fs;
use std::fs::File;
use std::io::Read; // untuk menggunakan function .read_to_string()

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;


    let mut calvin_file = File::open("calvin_with_dad.txt")?; // Buka file yang kita buat
    let mut calvin_string = String::new(); // String ini akan menyimpannya
    calvin_file.read_to_string(&mut calvin_string)?; // baca filenya dan letakkan di dalam mutable Stringnya

    calvin_string.split_whitespace().for_each(|word| print!("{} ", word.to_uppercase())); // melakukan sesuatu dengan String tersebut

    Ok(())
}

Hasilnya adalah:

CALVIN: DAD, HOW COME OLD PHOTOGRAPHS ARE ALWAYS BLACK AND WHITE? DIDN'T THEY HAVE COLOR FILM BACK THEN? DAD: SURE THEY DID. IN 
FACT, THOSE PHOTOGRAPHS *ARE* IN COLOR. IT'S JUST THE *WORLD* WAS BLACK AND WHITE THEN. CALVIN: REALLY? DAD: YEP. THE WORLD DIDN'T TURN COLOR UNTIL SOMETIMES IN THE 1930S...

Okay, bagaimana jika kita ingin membuat sebuah file namun kita tidak akan melakukannya jika di situ sudah ada file lainnya dengan nama yang sama? Mungkin Anda tidak ingin menghapus file lain tersebut (jika ia sudah terlebih dahulu ada disana), hanya karena ingin membuat satu file yang baru. Untuk melakukan ini, ada struct yang bernama OpenOptions. Sebenarnya, kita sudah menggunakan OpenOptions selama ini dan kita tidak mengetahuinya. Coba lihatlah source code dari File::open:


#![allow(unused)]
fn main() {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }
}

Menarik, ini mirip dengan builder pattern yang sebelumnya kita pelajari. Sama pula dengan File::create:


#![allow(unused)]
fn main() {
pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
    }
}

Jika Anda pergi ke laman tentang OpenOptions, Anda bisa melihat semua method yang bisa Anda gunakan. Kebanyakan method tersebut akan mengambil inputan bool:

  • append(): Ini berarti "tambahkan ke isi file tersebut (yang mana filenya sudah ada disana) alih-alih menghapus isinya".
  • create(): Ini memungkinkan OpenOptions membuat sebuah file.
  • create_new(): Ini berarti ia akan hanya membuat filenya jika filenya memang belum ada.
  • read(): Ubah ia menjadi true jika Anda ingin method tersebut bisa membaca sebuah file.
  • truncate(): Ubah ia menjadi true jika Anda ingin memotong isi dari filenya sampai ke 0 (menghapus isinya) di saat Anda membuka filenya.
  • write(): memungkinkan Anda menulis ke dalam file.

Dan kemudian, ada .open() dengan nama filenya, dan ia akan mengembalikan Result. Code di bawah ini adalah contohnya:

// ⚠️
use std::fs;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let calvin_file = OpenOptions::new().write(true).create_new(true).open("calvin_with_dad.txt")?;

    Ok(())
}

Pertama-tama, kita membuat sebuah OpenOptions menggunakan new (selalu dimulai dengan new). Kemudian kita memberikannya "kemampuan" untuk menulis (menggunakan write). Setelah itu, kita ubah create_new() menjadi true, dan mencoba membuka file yang kita buat. Dan ini tidak akan berhasil, sesuai seperti yang kita inginkan (karena sudah ada file dengan nama yang sama sebelumnya):

Error: Os { code: 80, kind: AlreadyExists, message: "The file exists." }

Mari kita coba menggunakan .append() untuk menuliskan sesuatu ke dalam file yang sudah ada itu. Untuk menulis ke dalam file, kita bisa menggunakan .write_all(), yang mana itu adalah method yang mencoba menuliskan apapun inputan yang kita berikan.

Dan juga, kita akan menggunakan macro write! untuk melakukan hal yang sama. Anda akan mengingat macro ini dari saat kita menggunakan impl Display untuk struct yang kita buat. Kali ini kita menggunakannya pada file.

use std::fs;
use std::fs::OpenOptions;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let mut calvin_file = OpenOptions::new()
        .append(true) // Now we can write without deleting it
        .read(true)
        .open("calvin_with_dad.txt")?;
    calvin_file.write_all(b"And it was a pretty grainy color for a while too.\n")?;
    write!(&mut calvin_file, "That's really weird.\n")?;
    write!(&mut calvin_file, "Well, truth is stranger than fiction.")?;

    println!("{}", fs::read_to_string("calvin_with_dad.txt")?);

    Ok(())
}

Hasilnya adalah:

Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...And it was a pretty grainy color for a while too.
That's really weird.
Well, truth is stranger than fiction.