The Rust Programing Language 10日目(12章 前半)
はじめに
いつの間にか数日経過していた
週末忙しかったので気を新たに再開、一度中断したものを再開できる勇気も大事
前回のあらすじ
前回はテストについてのさわりを知った
TDDがやりやすそうだなあという印象を覚えたのであった
本日の学び
12章 入出力プロジェクト: コマンドラインプログラムを構築するを読むぞ
今までで一番苦労しそうな予感があるのではじめに断っておくと、1章を複数日に分けて読むことも辞さない覚悟である
前提
ファイル名と文字列を受け取るgrep
を作成する、とのこと
grep
は何をするか想像できるのでとっつきやすいかもしれない
コマンドライン引数を受け付ける
引数の値を読み取る
標準ライブラリstd::env::args
を使ってコマンドライン引数を読み取れる
これはコマンドライン引数のイテレータを返す
イテレータに対してcollect
関数を呼び出すことで、コレクションに変換可能
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}
$ cargo run
--snip--
["target/debug/minigrep"]
$ cargo run needle haystack
--snip--
["target/debug/minigrep", "needle", "haystack"]
ベクタの最初の要素は"target/debug/minigrep"
になっているが、これはバイナリの名前
バイナリの名前を取れると嬉しいことも稀によくあるらしいがよくわかりません!
変数に入れるなどするときは添字1から始めるとコマンドライン引数を取得可能
ファイルを読む
filename
コマンドライン引数で指定されたファイルを読み込む
use std::env;
use std::fs::File;
use std::io::prelude::*;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("In file {}", filename);
// ファイルが見つかりませんでした
let mut f = File::open(filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
// ファイルの読み込み中に問題がありました
.expect("something went wrong reading the file");
// テキストは\n{}です
println!("With text:\n{}", contents);
}
リファクタリング
以下の4点を改善する
-
main
関数が引数の解析・ファイルの展開の2つの責務を負っている - 設定用変数が多くなると複雑化するため、構造体にしまう
- ファイルを開き損ねたとき、開けない理由に関わらず同じエラーメッセージを返却している
- 引数が不足した時のエラーが不親切
バイナリプロジェクトの責任の分離
main
の肥大化を防ぐための工程
- プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する。
- コマンドライン引数の解析ロジックが小規模な限り、main.rsに置いても良い。
- コマンドライン引数の解析ロジックが複雑化の様相を呈し始めたら、main.rsから抽出してlib.rsに移動する。
この工程の後にmain関数に残る責任は以下に限定される:
- 引数の値でコマンドライン引数の解析ロジックを呼び出す
- 他のあらゆる設定を行う
- lib.rsのrun関数を呼び出す
- runがエラーを返した時に処理する
main関数は読めばわかるくらい簡潔・小規模にする
引数解析器の抽出
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
// --snip--
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
設定値をまとめる
現在はparse_config
から受け取った値をタプルで受け取っているが、それをすぐ分解している
上記のような動作は正しい抽象化を行えていないサイン
configの2値も相互に関係するにも関わらず別の値になっているので、構造体に置き換える
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let mut f = File::open(config.filename).expect("file not found");
// --snip--
}
struct Config {
query: String,
filename: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config { query, filename }
}
元々はargs
のString
を参照する文字列スライスを返却していたが、Config
では所有するString
値を含むようにしている
clone
を使用するのは非効率だが、コードの単純化の観点から合理的
Configのコンストラクタを作成する
parse_config
の責務はConfig
のインスタンス生成なので、コンストラクタによって単純化する
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
// --snip--
}
// --snip--
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config { query, filename }
}
}
シンプルになった
読んでいてこっちの方が良くない?と思ったが、どうなのだろうか。。
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(args[1].clone(), args[2].clone());
// --snip--
}
// --snip--
impl Config {
fn new(query: String, filename: String) -> Config {
Config { query, filename }
}
}
上記の方がいいと思ったのは何となくだが、言語化するとしたらConfig
のコンストラクタにした時点で引数解析器の責務からはズレるので、コンストラクタの中で引数を読むのはおかしいのでは?ということだと思う
これは自分ならこうすると思うので、元の例の方が明らかに良いという観点があるなら教えてください
エラー処理を修正する
引数チェックが不十分なので追加する
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
// 引数の数が足りません
panic!("not enough arguments");
}
// --snip--
panicを呼ぶ代わりにnewからResultを返す
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
// 引数解析時に問題
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
impl Config {
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();
Ok(Config { query, filename })
}
}
unwrap_or_else
はpanic!
ではないエラーを定義できる
クロージャについては後ほど、、、
process::exit
でシステムを停止
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.48 secs
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
余計な出力が消えて見やすいね
mainからロジックを抽出する
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// --snip--
設定値のセットアップやエラー処理以外の部分を外に切り出したことで、main関数は簡潔になった
run関数はファイル読み込みから始まる全てのロジックを含むようになった
run関数からエラーを返す
Config::new
にしたように、エラー処理を改善する
use std::error::Error;
// --snip--
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
Ok(())
}
Box<dyn Error>
はいろんなエラー型を取れる。dynはdynamic
Ok(())
を返却するのは、run
を副作用のためにのみ呼ぶことを示す慣習
mainでrunから返却されたエラーを処理する
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
if let
でrun
がErr
を返したかどうかを確認する
コードをライブラリクレートに分割する
テストをするのと、main.rsファイルの責任を減らすために一部のコードをlib.rsに移動する
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
// --snip--
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// --snip--
}
pub
を使用して、テスト可能な公開APIを持つライブラリクレートを作成
これをmain.rsのバイナリクレートのスコープに持ち込む
extern crate minigrep;
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
if let Err(e) = minigrep::run(config) {
// --snip--
}
}
これでテストが書けるぞ!勝った!第一部完!
本日のまとめ
作業量が多い!!!
結論として1日で読むのは諦めた
ただ、構造化すべき部分を見極めたりライブラリクレートの具体的な分割方法など学びは多かった印象
Result
返すようにする辺りとか正直ちゃんとわかってない部分もあるので、後日また戻ってきて読みたい内容だった
一旦は完全に理解するよりも先に進むことと続けることを優先する
次回、12.4章
Discussion
のところで思ったことを書いてみます.
このプログラムにおいて
Config
が外部に見せるべきことはfilename
とquery
というフィールドがあるargs
を参照して作られるrun
に move されるであり,
・
args
の ( バイナリ名除く )1つめの要素をquery
, 2つめの要素をfilename
として構成されるというのは
Config
が ( 内部で ) 管理すべき ( なんなら, なにかの都合である日突然順番を入れ替えたりしてもいい ) もの, という観点からすると,だと
Config::new
を呼び出す側が ・ を知っていなければならないことになり, 不親切な API な感じがあります.・ は
Config::new
の中に閉じ込めて, 呼び出し側には 2. (args
を参照して作られる ) だけが分かるようにしたという形のほうが,
Config
の責務 (:コマンドライン引数をパースしてメインロジックに引き渡す ) がはっきりしていてよさそうに思いました.もっと言うと, 他で
args
を使わないのであれば, それ自体Config
に任せてみたいにしてしまう手もありそうです ( この場合, 初期化ロジックが複雑になってもテストを書きづらいというデメリットがありそうですが ) .
コメントありがとうございます!!(そもそも読んでくださってありがとうございます)
おかげで腑に落ちました、私はそもそもこの場合の
Congfig
の責務を勘違いしていました。は
Config
の責務ではなく、くらいに考えていました
そもそものgrepという目的や実装全体を見ても、この構造体はプログラム引数を管理する責務を持っていて、それを考えると引数の解釈は
Config
の中で完結すべきですねコンストラクタはあくまで
Config
の初期化以外のことはすべきではないので引数を解釈すべきでは無いと思っていましたが、Config
が引数から渡されるという前提に立てばそれは自然なことと理解しました重ねてありがとうございます!