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

モジュール性とエラーハンドリングを改善するためのリファクタリング

プログラムを改善するために、プログラムの構造と潜在的なエラーの 扱い方に関する4つの問題を修正します。まず、現在の main 関数は2つのタスクを実行しています。引数を解析することと、ファイルを 読み込むことです。プログラムが大きくなるにつれて、main 関数が扱う個別のタスクの数は増えていきます。関数の責務が増えるほど、 その関数を把握するのは難しくなり、テストもしにくくなり、どこか一部を 壊さずに変更することも難しくなります。各関数が1つのタスクだけを担当 するように、機能を分離するのが最善です。

この問題は2つ目の問題にも関係しています。queryfile_path はプログラムの設定変数ですが、contents のような変数は プログラムのロジックを実行するために使われます。main が長くなるほど、 スコープに持ち込む必要のある変数は増えます。スコープ内の変数が増える ほど、それぞれの目的を追跡するのは難しくなります。設定変数は1つの 構造体にまとめて、その目的を明確にするのが最善です。

3つ目の問題は、ファイルの読み込みに失敗したときにエラーメッセージを 表示するために expect を使っていますが、そのエラーメッセージが 単に Should have been able to read the file と表示されるだけである ことです。ファイルの読み込みはさまざまな理由で失敗する可能性があり ます。たとえば、ファイルが存在しないかもしれませんし、開く権限が ないかもしれません。現状では、状況にかかわらず、すべてに対して同じ エラーメッセージを表示することになり、ユーザーに何の情報も与えられ ません!

4つ目は、エラー処理に expect を使っているため、ユーザーが十分な数の 引数を指定せずにプログラムを実行すると、問題を明確に説明しない index out of bounds エラーを Rust から受け取ることです。 エラーハンドリングのロジックを変更する必要が生じたときに、将来の 保守担当者が参照すべきコードの場所が1つだけになるよう、すべての エラーハンドリング用コードが1か所にあるのが最善でしょう。 エラーハンドリング用コードを1か所にまとめることで、エンドユーザーに とって意味のあるメッセージを確実に表示できるようにもなります。

プロジェクトをリファクタリングして、これら4つの問題に対処しましょう。

バイナリプロジェクトで責務を分離する

複数のタスクの責務を main 関数に割り当ててしまうという構成上の問題は、 多くのバイナリプロジェクトに共通しています。そのため、多くの Rust プログラマーは、main 関数が大きくなり始めたら、バイナリプログラムの 別々の責務を分割するのが有用だと考えています。このプロセスは次の 手順からなります。

  • プログラムを main.rs ファイルと lib.rs ファイルに分け、プログラム のロジックを lib.rs に移す。
  • コマンドライン解析ロジックが小さいうちは、それを main 関数に残しておける。
  • コマンドライン解析ロジックが複雑になり始めたら、それを main 関数から他の関数や型へ切り出す。

このプロセスの後に main 関数に残る責務は、次のものに限定するべきです。

  • 引数の値を使ってコマンドライン解析ロジックを呼び出すこと
  • その他の設定を行うこと
  • lib.rsrun 関数を呼び出すこと
  • run がエラーを返した場合にそのエラーを処理すること

このパターンは責務の分離に関するものです。main.rs はプログラムの実行を 担当し、lib.rs は目の前のタスクのロジックをすべて担当します。 main 関数は直接テストできないため、この構造にすると、プログラムの ロジックを main 関数の外に移すことで、そのすべてをテストできるように なります。main 関数に残るコードは、読んで正しさを確認できるほど 小さくなります。このプロセスに従って、プログラムを作り直してみましょう。

引数パーサーを抽出する

引数を解析する機能を、main が呼び出す関数に抽出します。 リスト12-5は、新しい関数 parse_config を呼び出す main 関数の新しい冒頭部分を示しています。この関数は src/main.rs に 定義します。

use std::env;
use std::fs;

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

    let (query, file_path) = parse_config(&args);

    // --snip--

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

コマンドライン引数をベクタに収集している点は変わりませんが、 main 関数の中でインデックス1の引数値を変数 query に、 インデックス2の引数値を変数 file_path に代入する代わりに、 ベクタ全体を parse_config 関数に渡します。すると parse_config 関数が、どの引数をどの変数に入れるかを決定するロジックを保持し、 その値を main に返します。main の中で queryfile_path という変数は引き続き作成しますが、コマンドライン引数と変数を どのように対応付けるかを決定する責務は、もはや main にはあり ません。

この作り直しは、小さなプログラムに対してはやりすぎに見えるかも しれませんが、私たちは小さな段階的ステップでリファクタリングを 進めています。この変更を行ったら、引数の解析が引き続き機能する ことを確認するために、もう一度プログラムを実行してください。 問題が起きたときにその原因を特定しやすくするため、進捗をこまめに 確認するのは良いことです。

設定値をグループ化する

parse_config 関数をさらに改善するために、もう1つ小さなステップを 踏むことができます。現時点ではタプルを返していますが、その直後に そのタプルを再び個々の部分に分解しています。これは、まだ適切な 抽象化ができていないのかもしれないという兆候です。

改善の余地があることを示すもう1つの指標は、parse_configconfig という部分です。これは、返している2つの値が関連しており、 どちらも1つの設定値の一部であることを示唆しています。現在は、 2つの値をタプルにまとめる以外に、この意味をデータ構造の中で表現 できていません。そこで代わりに、2つの値を1つの struct に入れ、 各 struct フィールドに意味のある名前を付けます。そうすることで、 将来このコードを保守する人が、異なる値どうしがどのように関係して いるのか、そしてそれぞれの目的が何なのかを理解しやすくなります。

リスト12-6は、parse_config 関数に対する改善を示しています。

use std::env;
use std::fs;

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

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--

    println!("With text:\n{contents}");
}

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

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}

queryfile_path という名前のフィールドを持つように定義された Config という名前の struct を追加しました。parse_config の シグネチャは、返り値が Config 値であることを示すようになって います。以前は parse_config の本体で、args 内の String 値を参照する文字列スライスを返していましたが、今では Config に 所有権を持つ String 値を含めるように定義しています。mainargs 変数は引数値の所有者であり、parse_config 関数にはそれらを 借用させているだけなので、Configargs 内の値の所有権を取得 しようとすると、Rust の借用規則に違反することになります。

String データを扱う方法はいくつかあります。最も簡単なのは、 やや非効率ではあるものの、値に対して clone メソッドを呼び出す方法です。 これにより、Config インスタンスが所有するためのデータの完全なコピーが作られ、 文字列データへの参照を保持するよりも多くの時間とメモリを消費します。 しかし、データをクローンすると参照のライフタイムを管理する必要がなくなるため、 コードも非常に単純になります。この状況では、 単純さを得るために少しの性能を犠牲にするのは、価値のあるトレードオフです。

clone を使うことのトレードオフ

多くの Rustacean には、実行時コストのために所有権の問題を解決する目的で clone を使うのを避ける傾向があります。 第13章では、この種の状況でより効率的な方法を 使う方法を学びます。しかし今のところは、先に進むためにいくつかの文字列を コピーしても問題ありません。これらのコピーは一度しか行われず、ファイルパスと クエリ文字列も非常に小さいからです。最初の実装段階でコードを過度に最適化 しようとするよりも、少し非効率でも動作するプログラムを持つほうがよいのです。 Rust に慣れてくると、最も効率的な解決策から始めるのも簡単になりますが、 今の段階では clone を呼び出すことはまったく問題ありません。

main を更新し、parse_config が返す Config のインスタンスを config という名前の変数に格納するようにしました。また、以前は別々の query 変数と file_path 変数を使っていたコードも更新し、代わりに Config 構造体の フィールドを使うようにしました。

これで、queryfile_path が関係しており、その目的がプログラムの 動作方法を設定することであると、コードがより明確に伝えるようになりました。 これらの値を使うコードは、それらを config インスタンス内の、 その目的に応じた名前のフィールドに見つければよいことが分かります。

Config のコンストラクタを作成する

ここまでは、コマンドライン引数の解析を担当するロジックを main から切り出し、 parse_config 関数に配置してきました。そうすることで、queryfile_path の値が関係していることが見えてきました。そして、その関係はコードで表現される べきです。そこで Config 構造体を追加し、queryfile_path の 関連した目的に名前を付けるとともに、parse_config 関数から値の名前を構造体の フィールド名として返せるようにしました。

では、parse_config 関数の目的が Config インスタンスを作成することだと 分かったので、parse_config を単なる関数から、Config 構造体に関連付けられた new という名前の関数に変更できます。この変更により、コードはより イディオマティックになります。標準ライブラリ内の String のような型の インスタンスは、String::new を呼び出すことで作成できます。同様に、 parse_configConfig に関連付けられた new 関数に変更すれば、 Config::new を呼び出して Config のインスタンスを作成できるようになります。 リスト 12-7 は、必要な変更を示しています。

use std::env;
use std::fs;

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

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");

    // --snip--
}

// --snip--

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

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

main では、parse_config を呼び出していた箇所を更新し、代わりに Config::new を呼び出すようにしました。また、parse_config の名前を new に変更し、impl ブロックの中へ移動しました。これにより、new 関数は Config に関連付けられます。このコードが動作することを確認するために、 もう一度コンパイルしてみてください。

エラーハンドリングを修正する

次は、エラーハンドリングの修正に取り組みます。args ベクタのインデックス 1 または インデックス 2 の値にアクセスしようとすると、ベクタに 3 つ未満の要素しか 含まれていない場合にプログラムがパニックすることを思い出してください。 引数なしでプログラムを実行してみると、次のようになります。

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

index out of bounds: the len is 1 but the index is 1 という行は、 プログラマ向けのエラーメッセージです。これは、エンドユーザーが代わりに何を すべきかを理解する助けにはなりません。今からこれを修正しましょう。

エラーメッセージを改善する

リスト 12-8 では、インデックス 1 とインデックス 2 にアクセスする前に、 スライスが十分な長さを持っていることを確認するチェックを new 関数に追加します。 スライスの長さが足りない場合、プログラムはパニックし、よりよい エラーメッセージを表示します。

use std::env;
use std::fs;

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

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

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

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

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

        Config { query, file_path }
    }
}

このコードは、リスト 9-13 で書いた Guess::new 関数 に似ています。そこでは、value 引数が有効な値の範囲外だったときに panic! を呼び出しました。ここでは値の範囲を確認する代わりに、args の長さが少なくとも 3 であることを確認しており、関数の残りの部分は この条件が満たされているという前提で処理できます。args の要素数が 3 未満なら、 この条件は true になり、panic! マクロを呼び出してプログラムを即座に 終了します。

new にこの数行のコードを追加したので、引数なしでプログラムをもう一度実行し、 エラーが今どのように見えるかを確認してみましょう。

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

この出力は改善されています。これで、妥当なエラーメッセージが得られるように なりました。しかし、ユーザーには見せたくない余計な情報も含まれています。 おそらく、リスト 9-13 で使った手法はここで使うには最適ではありません。 第9章で説明したようにpanic! の呼び出しは使用上の問題よりもプログラミング上の問題に 適しています。代わりに、第9章で学んだもう 1 つの手法、つまり成功または エラーのいずれかを示す Result を返すこと を使います。

panic! を呼び出す代わりに Result を返す

代わりに Result 値を返すことで、成功した場合には Config インスタンスを、 エラーの場合には問題の内容を含められます。また、関数名を new から build に変更します。というのも、多くのプログラマは new 関数は決して失敗しないと 期待するからです。Config::buildmain とやり取りするときには、 Result 型を使って問題が発生したことを知らせられます。そうすれば、 main を変更して、Err バリアントを、panic! の呼び出しで生じる thread 'main'RUST_BACKTRACE に関する前後の文言なしの、 より実用的なエラーへと変換できます。

リスト 12-9 は、いま Config::build と呼んでいる関数の戻り値と、 Result を返すために必要な関数本体への変更を示しています。これについては、 次のリストで main も更新するまでコンパイルできないことに注意してください。

use std::env;
use std::fs;

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

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

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

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

        Ok(Config { query, file_path })
    }
}

私たちの build 関数は、成功時には Config インスタンスを、エラー時には文字列リテラルを含む Result を返します。エラー値は常に 'static ライフタイムを持つ文字列リテラルになります。

この関数の本体には 2 つの変更を加えました。ユーザーが十分な数の引数を渡さなかったときに panic! を呼び出す代わりに Err 値を返すようにし、さらに Config の戻り値を Ok で包みました。これらの変更により、この関数は新しい型シグネチャに適合します。

Config::build から Err 値を返すことで、main 関数は build 関数から返された Result 値を 処理し、エラー時によりクリーンにプロセスを終了できるようになります。

Config::build を呼び出してエラーを処理する

エラー時を処理してユーザーフレンドリーなメッセージを表示するには、 リスト 12-10 に示すように、Config::build から返される Result を処理するよう main を更新する必要があります。また、コマンドラインツールをゼロ以外のエラーコードで終了させる責務を panic! から切り離し、代わりに自前で実装します。ゼロ以外の終了ステータスは、私たちのプログラムを呼び出した プロセスに対して、そのプログラムがエラー状態で終了したことを知らせる慣例です。

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

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

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

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

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

        Ok(Config { query, file_path })
    }
}

このリストでは、まだ詳しく扱っていないメソッド unwrap_or_else を使っています。これは標準ライブラリによって Result<T, E> に定義されています。 unwrap_or_else を使うと、panic! ではない独自のエラー処理を定義できます。 ResultOk 値であれば、このメソッドの振る舞いは unwrap と似ています。つまり、Ok が包んでいる内部の値を返します。しかし、その 値が Err 値であれば、このメソッドはクロージャ内のコードを呼び出します。クロージャとは、私たちが定義して unwrap_or_else に引数として渡す無名関数です。 クロージャについては 第13章 でさらに詳しく扱います。今のところは、 unwrap_or_elseErr の内部の値を渡す、ということだけ知っていれば十分です。この場合、その値は リスト 12-9 で追加した静的文字列 "not enough arguments" であり、縦棒の間に現れる引数 err を通じて クロージャに渡されます。すると、クロージャ内のコードは実行時にその err 値を使うことができます。

標準ライブラリから process をスコープに導入するために、新しい use 行を追加しました。 エラー時に実行されるクロージャ内のコードは 2 行だけです。err 値を表示し、その後で process::exit を呼び出します。process::exit 関数はプログラムを即座に停止し、 終了ステータスコードとして渡された数値を返します。これは リスト 12-8 で使った panic! ベースの処理に似ていますが、余分な出力がすべて表示されることはなくなります。 試してみましょう。

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

すばらしいですね。この出力は、ユーザーにとってずっと親切です。

main からロジックを抽出する

設定のパースのリファクタリングが終わったので、次は プログラムのロジックに移りましょう。「バイナリプロジェクトで関心を分離する」 で述べたとおり、 設定の準備やエラー処理に関わらない、現在 main 関数にあるすべてのロジックを保持する run という名前の関数を抽出します。これが終わると、main 関数は簡潔で、目で確認するだけでも検証しやすくなり、 それ以外のすべてのロジックに対してテストを書けるようになります。

リスト 12-11 は、run 関数を抽出する小さな段階的改善を示しています。

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

fn main() {
    // --snip--

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

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

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

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

        Ok(Config { query, file_path })
    }
}

run 関数には、ファイルの読み込みから始まる、main に残っていたすべてのロジックが 入るようになりました。run 関数は Config インスタンスを 引数として受け取ります。

run からエラーを返す

残りのプログラムロジックを run 関数に分離したので、 リスト 12-9 の Config::build と同じように、エラー処理を改善できます。 expect を呼び出してプログラムを panic させるのではなく、何か問題が起きたときに run 関数が Result<T, E> を返すようにします。こうすることで、 ユーザーフレンドリーな形でのエラー処理のロジックを、さらに main に集約できます。 リスト 12-12 は、run のシグネチャと本体に必要な変更を示しています。

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

// --snip--


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

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

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

    println!("With text:\n{contents}");

    Ok(())
}

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

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

        Ok(Config { query, file_path })
    }
}

ここでは 3 つの重要な変更を行いました。まず、run 関数の戻り値の型を Result<(), Box<dyn Error>> に変更しました。この関数は以前は ユニット型 () を返しており、Ok の場合に返す値としてはそれを維持しています。

エラー型には、トレイトオブジェクト Box<dyn Error> を使いました(そして 先頭の use 文で std::error::Error をスコープに導入しました)。トレイトオブジェクトについては 第18章 で扱います。今のところは、Box<dyn Error> が その関数が Error トレイトを実装する型を返すことを意味しつつも、 戻り値がどの具体的な型になるかまでは指定する必要がない、ということだけ知っていれば十分です。 これにより、異なるエラー時に異なる型のエラー値を返せる 柔軟性が得られます。dyn キーワードは dynamic の略です。

次に、第9章 で説明したように、 expect の呼び出しを削除して ? 演算子を使うようにしました。 エラー時に panic! する代わりに、? は現在の関数からエラー値を返し、 呼び出し元に処理を委ねます。

3 つ目に、run 関数は成功時に Ok 値を返すようになりました。 シグネチャで run 関数の成功型を () と宣言しているため、 ユニット型の値を Ok 値で包む必要があります。この Ok(()) という構文は、最初は少し奇妙に見えるかもしれません。しかし、このように () を使うのは、 副作用のためだけに run を呼び出していることを示す慣用的な方法です。 必要な値を返しているわけではありません。

このコードを実行すると、コンパイルは通りますが警告が表示されます。

$ cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
19 |     let _ = run(config);
   |     +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust は、私たちのコードが Result 値を無視しており、その Result 値は エラーが発生したことを示しているかもしれない、と教えてくれています。しかし、実際に エラーがあったかどうかを確認しておらず、コンパイラはここに何らかの エラー処理コードを書くつもりだったはずだと警告してくれます。では、この問題を今すぐ修正しましょう。

mainrun から返されるエラーを処理する

エラーを確認し、リスト 12-10 の Config::build で使用したものと似た手法で処理しますが、少し違いがあります。

ファイル名: src/main.rs

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

fn main() {
    // --snip--

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

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

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

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

    println!("With text:\n{contents}");

    Ok(())
}

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

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

        Ok(Config { query, file_path })
    }
}

runErr 値を返したかどうかを確認し、返した場合に process::exit(1) を呼び出すために、unwrap_or_else ではなく if let を使います。run 関数は、Config::buildConfig インスタンスを返すのと同じ形で unwrap したい値を返しません。run は成功時には () を返すため、私たちが気にするのはエラーを検出することだけです。したがって、アンラップした値を返す unwrap_or_else は必要ありません。その値も () にすぎないからです。

if letunwrap_or_else の本体は、どちらの場合も同じです。エラーを表示して終了します。

コードをライブラリクレートに分割する

ここまでで minigrep プロジェクトはかなり良い状態になっています。次は src/main.rs ファイルを分割し、いくつかのコードを src/lib.rs ファイルに移します。そうすることで、コードをテストできるようになり、src/main.rs ファイルの責務も少なくできます。

テキスト検索を担当するコードは src/main.rs ではなく src/lib.rs に定義しましょう。そうすることで、私たち自身(あるいは minigrep ライブラリを使う他の誰か)が、minigrep バイナリよりも多くのコンテキストから検索関数を呼び出せるようになります。

まず、リスト 12-13 に示すように、src/lib.rssearch 関数のシグネチャを定義し、本体では unimplemented! マクロを呼び出すようにします。シグネチャについては、実装を埋めるときにより詳しく説明します。

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}

関数定義に pub キーワードを使って、search をライブラリクレートの公開 API の一部として指定しました。これで、バイナリクレートから利用でき、テストもできるライブラリクレートができました。

次に、src/lib.rs で定義したコードを src/main.rs のバイナリクレートのスコープに持ち込み、それを呼び出す必要があります。これはリスト 12-14 に示されています。

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

// --snip--
use minigrep::search;

fn main() {
    // --snip--
    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);
    }
}

// --snip--


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

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

        Ok(Config { query, file_path })
    }
}

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

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

ライブラリクレートから search 関数をバイナリクレートのスコープに持ち込むために、use minigrep::search という行を追加します。次に run 関数では、ファイルの内容を表示する代わりに search 関数を呼び出し、config.query の値と contents を引数として渡します。そして run は、for ループを使って、クエリに一致した search の返り値の各行を表示します。また、エラーが発生しない限りプログラムが検索結果だけを表示するように、クエリとファイルパスを表示していた main 関数内の println! 呼び出しを削除するのにも良いタイミングです。

検索関数は、表示が行われる前に、返り値となるベクタにすべての結果を集めることに注意してください。この実装は、大きなファイルを検索する場合に結果の表示が遅くなる可能性があります。結果は見つかるたびに表示されるのではないからです。これをイテレータを使って改善する方法については、第 13 章で説明します。

ふう! かなりの作業でしたが、将来の成功に向けた土台を整えることができました。これでエラー処理はずっと簡単になり、コードもよりモジュール化されました。これ以降の作業のほとんどは src/lib.rs で行うことになります。

この新たに得られたモジュール性を活かして、古いコードでは難しかったが新しいコードなら簡単なことをしてみましょう。テストを書きます!