Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Meningkatkan Project I/O Kita

Dengan pengetahuan baru tentang iterators ini, kita bisa meningkatkan project I/O di Bab 12 dengan memakai iterators buat bikin beberapa bagian kodenya jadi lebih jelas dan lebih ringkas. Mari kita lihat gimana iterators bisa meningkatkan implementasi dari fungsi Config::build dan fungsi search kita.

Menghilangkan clone Menggunakan Iterator

Di Listing 12-6, kita menambahkan kode yang mengambil slice berisi nilai String lalu membuat instance dari struct Config dengan mengindeks ke dalam slice tersebut dan meng-clone (menyalin) nilai-nilainya, yang memungkinkan struct Config buat memiliki (own) nilai-nilai itu. Di Listing 13-17, kita menampilkan ulang implementasi dari fungsi Config::build persis seperti yang ada di Listing 12-23.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-17: Menampilkan ulang fungsi Config::build dari Listing 12-23

Waktu itu, kita bilang buat tidak mengkhawatirkan panggilan clone yang kurang efisien karena kita bakal menghapusnya di masa depan. Nah, sekaranglah saatnya!

Kita membutuhkan clone di sini karena kita punya sebuah slice berisi elemen-elemen String di parameter args, tapi fungsi build tidak mengambil kepemilikan (ownership) atas args. Buat mengembalikan kepemilikan dari instance Config, kita harus meng-clone nilai-nilai dari field query dan file_path milik Config supaya instance Config tersebut bisa memiliki nilai-nilainya.

Dengan pengetahuan baru kita tentang iterators, kita bisa mengubah fungsi build agar mengambil kepemilikan dari sebuah iterator sebagai argumennya, ketimbang meminjam (borrow) sebuah slice. Kita bakal memakai fungsionalitas iterator alih-alih kode yang mengecek panjang dari slice dan mengindeks ke lokasi spesifik. Ini bakal memperjelas apa yang sedang dilakukan oleh fungsi Config::build karena iterator-lah yang bakal mengakses nilai-nilainya.

Begitu Config::build mengambil kepemilikan atas iterator dan berhenti memakai operasi indexing yang sifatnya meminjam, kita bisa memindahkan (move) nilai-nilai String dari iterator tersebut ke dalam Config alih-alih memanggil clone dan membuat alokasi baru.

Memakai Iterator yang Dikembalikan secara Langsung

Buka file src/main.rs dari project I/O kita, yang seharusnya kelihatan seperti ini:

Nama file: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Pertama-tama kita bakal mengubah bagian awal dari fungsi main yang kita punya di Listing 12-24 jadi kode yang ada di Listing 13-18, yang mana kali ini memakai iterator. Ini belum bisa di-compile sampai kita meng-update Config::build juga.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-18: Meneruskan nilai yang dikembalikan oleh env::args ke Config::build

Fungsi env::args mengembalikan sebuah iterator! Alih-alih mengumpulkan nilai-nilai iterator itu ke dalam sebuah vector terus meneruskan sebuah slice ke Config::build, sekarang kita meneruskan kepemilikan dari iterator yang dikembalikan dari env::args ke Config::build secara langsung.

Berikutnya, kita harus meng-update definisi dari Config::build. Mari kita ubah signature (tanda tangan) dari Config::build agar kelihatan seperti Listing 13-19. Ini masih belum bisa di-compile, karena kita harus meng-update isi (body) fungsinya.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-19: Meng-update signature dari Config::build buat mengharapkan sebuah iterator

Dokumentasi standard library untuk fungsi env::args menunjukkan bahwa tipe dari iterator yang dikembalikannya adalah std::env::Args, dan tipe tersebut mengimplementasikan trait Iterator serta mengembalikan nilai String.

Kita sudah meng-update signature dari fungsi Config::build jadi parameter args punya tipe generik dengan trait bounds impl Iterator<Item = String> bukannya &[String]. Penggunaan sintaks impl Trait yang kita bahas di bagian “Traits sebagai Parameter” di Bab 10 ini berarti args bisa berupa tipe apa pun yang mengimplementasikan trait Iterator dan mengembalikan item berupa String.

Karena kita mengambil kepemilikan atas args dan kita bakal memutasi (mengubah) args saat kita beriterasi melewatinya, kita bisa menambahkan keyword mut ke dalam spesifikasi parameter args buat membikinnya jadi mutable.

Memakai Method Trait Iterator Alih-Alih Indexing

Berikutnya, kita bakal memperbaiki isi dari Config::build. Karena args mengimplementasikan trait Iterator, kita tahu kalau kita bisa memanggil method next padanya! Listing 13-20 meng-update kode dari Listing 12-23 buat memakai method next.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-20: Mengubah isi dari Config::build buat memakai method iterator

Ingat kembali bahwa nilai pertama di dalam nilai yang dikembalikan oleh env::args adalah nama dari programnya. Kita mau mengabaikan itu dan lanjut ke nilai berikutnya, jadi pertama kita memanggil next dan tidak ngelakuin apa-apa dengan nilai yang dikembalikannya. Terus kita panggil next lagi buat dapet nilai yang mau kita masukin ke field query dari Config. Kalau next mengembalikan Some, kita pakai match buat mengekstrak nilainya. Kalau dia mengembalikan None, itu berarti argumen yang diberikan tidak cukup dan kita bisa keluar lebih awal dengan nilai Err. Kita ngelakuin hal yang sama buat nilai file_path.

Membikin Kode Lebih Jelas dengan Iterator Adapters

Kita juga bisa memanfaatkan iterators di fungsi search dari project I/O kita, yang ditampilkan ulang di Listing 13-21 persis seperti yang ada di Listing 12-19.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 13-21: Implementasi dari fungsi search dari Listing 12-19

Kita bisa nulis kode ini dengan cara yang lebih ringkas memakai method iterator adapter. Dengan begitu, kita juga terhindar dari kewajiban punya vector results menengah (intermediate) yang mutable. Gaya pemrograman fungsional lebih suka meminimalisir mutable state (keadaan yang bisa berubah) demi bikin kodenya jadi lebih jelas. Membuang mutable state ini mungkin bisa memungkinkan adanya peningkatan di masa depan yang bakal bikin pencarian terjadi secara paralel karena kita tidak perlu pusing mengelola akses konruen (concurrent access) ke vector results tersebut. Listing 13-22 menunjukkan perubahan ini.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 13-22: Memakai method iterator adapter di implementasi fungsi search

Ingat kembali bahwa tujuan dari fungsi search adalah mengembalikan semua baris di dalam contents yang mengandung query. Mirip seperti contoh filter di Listing 13-16, kode ini memakai adapter filter buat menyimpan cuma baris-baris di mana line.contains(query) mengembalikan true. Kita kemudian mengumpulkan baris-baris yang cocok itu ke dalam sebuah vector lain menggunakan collect. Jauh lebih simpel kan! Jangan ragu buat ngelakuin perubahan yang sama buat memakai method iterator di fungsi search_case_insensitive juga.

Sebagai peningkatan lanjutan, coba kembalikan sebuah iterator dari fungsi search dengan menghapus panggilan ke collect dan mengubah tipe kembaliannya jadi impl Iterator<Item = &'a str> supaya fungsi ini menjadi sebuah iterator adapter. Perhatikan bahwa kita juga bakal harus meng-update pengujiannya! Coba cari di dalam sebuah file berukuran besar menggunakan alat minigrep kita sebelum dan sesudah membikin perubahan ini untuk melihat perbedaan perilakunya. Sebelum perubahan ini, program tidak bakal mencetak hasil apa pun sampai ia selesai mengumpulkan semua hasilnya, tapi setelah perubahan itu, hasil bakal dicetak satu per satu setiap kali ada baris yang cocok karena for loop di fungsi run bisa memanfaatkan sifat lazy (malas) dari iterator-nya.

Memilih antara Loops dan Iterators

Pertanyaan masuk akal berikutnya adalah gaya mana yang sebaiknya kita pilih di kode kita sendiri dan kenapa: implementasi awal di Listing 13-21 atau versi yang memakai iterators di Listing 13-22 (dengan asumsi kita mengumpulkan semua hasilnya sebelum mengembalikannya bukannya mengembalikan iteratornya). Sebagian besar programmer Rust lebih suka memakai gaya iterator. Mungkin agak sedikit susah buat memahaminya di awal, tapi begitu kita sudah mulai merasakan (get a feel for) berbagai iterator adapters dan apa yang mereka lakukan, iterators bisa jadi lebih gampang buat dipahami. Ketimbang ribet ngurusin detail soal looping (perulangan) dan membikin vector baru, kode kita jadi bisa lebih fokus ke tujuan tingkat tinggi (high-level objective) dari loop tersebut. Ini mengabstraksi kode-kode umum (commonplace code) sehingga konsep-konsep yang unik buat kode ini jadi lebih mudah dilihat, seperti contohnya kondisi penyaringan (filtering condition) yang harus dilewati sama setiap elemen di dalam iterator.

Tapi apakah kedua implementasi ini benar-benar ekuivalen (sama)? Asumsi yang mungkin muncul secara intuitif adalah loop tingkat rendah (lower-level loop) bakal lebih cepat. Mari kita bahas soal performa.