入出力プロジェクトを改善する

このイテレータに関する新しい知識があれば、イテレータを使用してコードのいろんな場所をより明確で簡潔にすることで、 第12章の入出力プロジェクトを改善することができます。イテレータがConfig::build関数とsearch関数の実装を改善する方法に目を向けましょう。

イテレータを使用してcloneを取り除く

リスト12-6において、スライスに添え字アクセスして値をクローンすることで、Config構造体に値を所有させながら、 String値のスライスを取り、Config構造体のインスタンスを作るコードを追記しました。リスト13-17では、 リスト12-23のようなConfig::buildの実装を再現しました:

ファイル名: src/lib.rs

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

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

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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
}

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)
        );
    }
}

リスト13-17: リスト12-23からConfig::build関数の再現

その際、将来的に除去する予定なので、非効率的なclone呼び出しを憂慮するなと述べました。 えっと、その時は今です!

引数argsString要素のスライスがあるためにここでcloneが必要だったのですが、 build関数はargsを所有していません。Configインスタンスの所有権を返すためには、 Configインスタンスがその値を所有できるように、Configqueryfile_pathフィールドから値をクローンしなければなりませんでした。

イテレータについての新しい知識があれば、build関数をスライスを借用する代わりに、 引数としてイテレータの所有権を奪うように変更することができます。スライスの長さを確認し、 特定の場所に添え字アクセスするコードの代わりにイテレータの機能を使います。これにより、 イテレータは値にアクセスするので、Config::build関数がすることが明確化します。

ひとたび、Config::buildがイテレータの所有権を奪い、借用する添え字アクセス処理をやめたら、 cloneを呼び出して新しくメモリ確保するのではなく、イテレータからのString値をConfigにムーブできます。

返却されるイテレータを直接使う

入出力プロジェクトのsrc/main.rsファイルを開いてください。こんな見た目のはずです:

ファイル名: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

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) = minigrep::run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

まずはリスト12-24のようなmain関数の冒頭を、今回はイテレータを使用するリスト13-18のコードに変更します。 これは、Config::buildも更新するまでコンパイルできません。

ファイル名: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

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) = minigrep::run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

リスト13-18: env::argsの戻り値をConfig::buildに渡す

env::args関数は、イテレータを返します!イテレータの値をベクタに集結させ、それからスライスをConfig::buildに渡すのではなく、 今ではenv::argsから返ってくるイテレータの所有権を直接Config::buildに渡しています。

次に、Config::buildの定義を更新する必要があります。入出力プロジェクトのsrc/lib.rsファイルで、 Config::buildのシグニチャをリスト13-19のように変えましょう。関数本体を更新する必要があるので、 それでもコンパイルはできません。

ファイル名: src/lib.rs

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

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

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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
}

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)
        );
    }
}

リスト13-19: Config::buildのシグニチャをイテレータを期待するように更新する

env::args関数の標準ライブラリドキュメントは、この関数が返すイテレータの型はstd::env::Argsであること、 そしてこの型はString値を返すIteratorトレイトを実装していることを示しています。

引数argsの型が&[String]ではなく、トレイト境界impl Iterator<Item = String>を持つジェネリック型を持つように、 Config::build関数のシグニチャを更新しています。第10章の「引数としてのトレイト」節で議論したimpl Trait構文のここでの使用は、 argsIterator型を実装しString要素を返す任意の型でよいことを意味します。

argsの所有権を奪い、繰り返しを行うことでargsを可変化する予定なので、 args引数の仕様にmutキーワードを追記でき、可変にします。

添え字の代わりにIteratorトレイトのメソッドを使用する

次に、Config::buildの本体を修正しましょう。argsIteratorトレイトを実装しているので、 それに対してnextメソッドを呼び出せることがわかります!リスト13-20は、 リスト12-23のコードをnextメソッドを使用するように更新したものです:

ファイル名: src/lib.rs

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

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

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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
}

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)
        );
    }
}

リスト13-20: Config::buildの本体をイテレータメソッドを使うように変更する

env::argsの戻り値の1番目の値は、プログラム名であることを思い出してください。それは無視し、 次の値を取得したいので、まずnextを呼び出し、戻り値に対して何もしません。2番目に、 nextを呼び出してConfigqueryフィールドに置きたい値を得ます。nextSomeを返したら、 matchを使用してその値を抜き出します。Noneを返したら、十分な引数が与えられなかったということなので、 Err値で早期リターンします。file_path値に対しても同じことをします。

イテレータアダプタでコードをより明確にする

入出力プロジェクトのsearch関数でも、イテレータを活用することができます。その関数はリスト12-19に示していますが、以下のリスト13-28に再掲します。

ファイル名: src/lib.rs

use std::error::Error;
use std::fs;

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

impl Config {
    pub 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();

        Ok(Config { query, file_path })
    }
}

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

    Ok(())
}

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));
    }
}

リスト13-21: リスト12-19のsearch関数の実装

イテレータアダプタメソッドを使用して、このコードをもっと簡潔に書くことができます。そうすれば、 可変な中間のresultsベクタをなくすこともできます。関数型プログラミングスタイルは、可変な状態の量を最小化することを好み、 コードを明瞭化します。可変な状態を除去すると、検索を同時並行に行うという将来的な改善をするのが、 可能になる可能性があります。なぜなら、resultsベクタへの同時アクセスを管理する必要がなくなるからです。 リスト13-22は、この変更を示しています:

ファイル名: src/lib.rs

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

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

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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)
        );
    }
}

リスト13-22: search関数の実装でイテレータアダプタのメソッドを使用する

search関数の目的は、queryを含むcontentsの行全てを返すことであることを思い出してください。 リスト13-16のfilter例に酷似して、このコードはfilterアダプタを使用してline.contains(query)trueを返す行だけを残すことができます。 それから、合致した行を別のベクタにcollectで集結させます。ずっと単純です!ご自由に、 同じ変更を行い、search_case_insensitive関数でもイテレータメソッドを使うようにしてください。

ループかイテレータかの選択

次の論理的な疑問は、自身のコードでどちらのスタイルを選ぶかと理由です: リスト13-21の元の実装とリスト13-22のイテレータを使用するバージョンです。 多くのRustプログラマは、イテレータスタイルを好みます。とっかかりが少し困難ですが、 いろんなイテレータアダプタとそれがすることの感覚を一度掴めれば、イテレータの方が理解しやすいこともあります。 いろんなループを少しずつもてあそんだり、新しいベクタを構築する代わりに、コードは、ループの高難度の目的に集中できるのです。 これは、ありふれたコードの一部を抽象化するので、イテレータの各要素が通過しなければならないふるい条件など、 このコードに独特の概念を理解しやすくなります。

ですが、本当に2つの実装は等価なのでしょうか?直観的な仮説は、より低レベルのループの方がより高速ということかもしれません。 パフォーマンスに触れましょう。