入出力プロジェクトを改善する
このイテレータに関する新しい知識があれば、イテレータを使用してコードのいろんな場所をより明確で簡潔にすることで、
第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呼び出しを憂慮するなと述べました。
えっと、その時は今です!
引数argsにString要素のスライスがあるためにここでcloneが必要だったのですが、
build関数はargsを所有していません。Configインスタンスの所有権を返すためには、
Configインスタンスがその値を所有できるように、Configのqueryとfile_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構文のここでの使用は、
argsはIterator型を実装しString要素を返す任意の型でよいことを意味します。
argsの所有権を奪い、繰り返しを行うことでargsを可変化する予定なので、
args引数の仕様にmutキーワードを追記でき、可変にします。
添え字の代わりにIteratorトレイトのメソッドを使用する
次に、Config::buildの本体を修正しましょう。argsはIteratorトレイトを実装しているので、
それに対して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を呼び出してConfigのqueryフィールドに置きたい値を得ます。nextがSomeを返したら、
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つの実装は等価なのでしょうか?直観的な仮説は、より低レベルのループの方がより高速ということかもしれません。 パフォーマンスに触れましょう。