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

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

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

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

ファイル名: src/lib.rs

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

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

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

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

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

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

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

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

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

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

ファイル名: src/main.rs

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

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

    // --snip--
}

リスト12-24のようなmain関数の冒頭をリスト13-25のコードに変更します。 これは、Config::newも更新するまでコンパイルできません。

ファイル名: src/main.rs

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

    // --snip--
}

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

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

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

ファイル名: src/lib.rs

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        // --snip--

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

env::args関数の標準ライブラリドキュメントは、自身が返すイテレータの型は、std::env::Argsであると表示しています。 Config::new関数のシグニチャを更新したので、引数argsの型は、&[String]ではなく、 std::env::Argsになりました。argsの所有権を奪い、繰り返しを行うことでargsを可変化する予定なので、 args引数の仕様にmutキーワードを追記でき、可変にします。

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

次に、Config::newの本体を修正しましょう。標準ライブラリのドキュメントは、 std::env::ArgsIteratorトレイトを実装していることにも言及しているので、 それに対してnextメソッドを呼び出せることがわかります!リスト13-27は、 リスト12-23のコードをnextメソッドを使用するように更新したものです:

ファイル名: src/lib.rs

fn main() {}
use std::env;

struct Config {
    query: String,
    filename: String,
    case_sensitive: bool,
}

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            // クエリ文字列を取得しませんでした
            None => return Err("Didn't get a query string"),
        };

        let filename = match args.next() {
            Some(arg) => arg,
            // ファイル名を取得しませんでした
            None => return Err("Didn't get a file name"),
        };

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

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

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

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

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

ファイル名: 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
}

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

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

ファイル名: src/lib.rs

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

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

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

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

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

関連キーワード:  Config, リスト, new, args, 関数, コード, env, プロジェクト, query, 入出力