三行
- cargoはenv_loggerとdocoptってcrateを使ってるよ。
- Rustの統一されたエラー処理の初歩が分かるよ。
- イテレータアダプタも面白いよ。
main()
関数
前回、cargoのmain()
関数がsrc/bin/cargo.rs
にあることを突き止めました:
% cargo metadata --no-deps | jq '.packages[].targets[] | select(.kind==["bin"])'
{
"kind": [
"bin"
],
"name": "cargo",
"src_path": "src/bin/cargo.rs"
}
というわけで、ようやくmain()
関数にたどり着くことができました:
fn main() {
env_logger::init().unwrap();
execute_main_without_stdin(execute, true, USAGE)
}
env_logger crate
まず、一行目のenv_logger::init()
関数の呼び出しが目に留まります。Racer for emacsを使っていれば、このinit
のところにカーソルを合わせてM-.を押すと、このソースに飛ぶことができます。私の環境では~/.cargo/registry/src/github.com-1ecc6299db9ec823/env_logger-0.3.5/src/lib.rs
というファイルに飛びました。
env_logger crateのCargo.toml
を読むと、これはRustプロジェクトが開発しているlogライブラリの一部であることが想像できます:
[package]
name = "env_logger"
version = "0.3.5"
authors = ["The Rust Project Developers"]
license = "MIT/Apache-2.0"
repository = "https://github.com/rust-lang/log"
documentation = "http://doc.rust-lang.org/log/env_logger"
homepage = "https://github.com/rust-lang/log"
このenv_loggerのリポジトリを覗いてみます。なお、上記repository
のURLは既に無効で、現在のリポジトリはhttps://github.com/rust-lang-nursery/logです1。https://github.com/rust-lang-nursery/log/README.mdを読むと、次のようなことがわかります:
- logライブラリは
log
crateとenv_logger
crateから成る。 -
log
crateは、ログを生成する側へのfacadeを提供する。 -
env_logger
crateは、ログを実際に標準エラー出力などに吐き出すバックエンドを提供する。
このように二つの層に分けることによって、ログのバックエンドを自由に差し替えられるように設計されています。env_logger crateは一番シンプルなバックエンド実装を提供しています。
ついでに、軽くlog
crateの方にも触れておきます。cargoのソースを読んでいると、debug!
やら error!
やら warn!
やらといったマクロに出会います。これがlog
crateの提供しているログ出力関数ですので覚えておくとよいでしょう。
env_logger::init().unwrap()
env_logger
の素性が分かったので、main()
関数に戻ってenv_logger::init().unwrap()
という行について吟味してみましょう。env_logger::init()
関数のリファレンスを見ると、次のようなインターフェースであることが分かります:
pub fn init() -> Result<(), SetLoggerError>
これは興味深いイディオムですね。つまり、正味の戻り値がない関数の場合でも、エラーを単体で返すのではなく、Result
型でくるんで返すことにより、統一的なエラーハンドリングができるというわけです。ここでは単純にunwrap()
を呼び出していますが、RustのResult
型のunwrap()
関数は、内部でpanic!
マクロを呼んでいるため、安全に終了することができるようになっています2。
unwrap()
を忘れると大変なことになりそうな気も、と思うかもしれませんが、Result
型の定義は次のようになっています:
# [derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
# [must_use]
# [stable(feature = "rust1", since = "1.0.0")]
pub enum Result<T, E> {
...
この#[must_use]
アトリビュートのおかげで、結果を一切使わずに捨ててしまうとコンパイラが次のような警告を発します:
warning: unused result which must be used, #[warn(unused_must_use)] on by default
--> src/bin/cargo.rs:70:5
|
70 | env_logger::init();
| ^^^^^^^^^^^^^^^^^^^
なお、関数の名前から明らかなように、このenv_logger::init()
関数はenv_logger crateの初期化を行います:
Initializes the global logger with an env logger.
This should be called early in the execution of a Rust program, and the global logger may only be initialized once. Future initialization attempts will return an error.
execute_main_without_stdin()
関数
main()
関数の次の行はexecute_main_without_stdin()
関数の呼び出しです。execute_main_without_stdin()
関数はsrc/cargo/lib.rs
で実装されています(GitHub)。コードを眺めてみると、入れ子構造で若干分かりにくいものの、次のような構造になっています:
execute_main_without_stdin()
|
+--- process()
|
+--- ||() // 無名関数
|
+--- call_main_without_stdin()
|
+--- exec() // execクロージャとしてsrc/bin/cargo.rsのexecute()関数が渡されている
これらは、親プロセスから受け渡された引数や環境変数に基づいて、cargoの実行に必要となる基本的なコンフィグレーション情報を構築するルーチンのようです。このように入れ子にするスタイルは、C言語などに慣れているとちょっと奇異な感じもしますが、Rustではジェネリクスなどの豊富な文脈を用いることにより自然に書くことができるため、このように書くとスコープに基づいたライフタイムとの相性が良いのだと思います3。
ところで、execute_main_without_stdin()
関数はジェネリック関数となっています。exec
引数は実際のサブコマンドの処理を行うクロージャですが、このクロージャの第一引数の型Tと、正味の戻り値の型Vについては我関せずということを表明しています。execute_main_without_stdin()
関数が再利用されるとも考えにくいので、つまりこれは、再利用のための柔軟性を与えるためにジェネリクスを使用しているのではなく、型の詳細について、自分の責任範囲と関係ない部分については関心を持たないということを表明するためにジェネリクスを使用しているのだと思います。Rustではトレイトによって必要最低限の型制約を与えることができるため、C++のテンプレートのように実体化するまで何が起こるか分からないことに起因する欠点4がほとんどなく、このように分離することで純粋な構造だけに注力できるようになるため、これは良いスタイルなのではないでしょうか。
process()
関数
次にprocess()
関数を見てみましょう。
fn process<V, F>(mut callback: F)
where F: FnMut(&[String], &Config) -> CliResult<Option<V>>,
V: Encodable
{
let mut config = None;
let result = (|| {
config = Some(Config::default()?);
let args: Vec<_> = try!(env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect());
callback(&args, config.as_ref().unwrap())
})();
let mut verbose_shell = shell(Verbose, Auto);
let mut shell = config.as_ref().map(|s| s.shell());
let shell = shell.as_mut().map(|s| &mut **s).unwrap_or(&mut verbose_shell);
process_executed(result, shell)
}
謎の無名関数
このコードを見て、少し不思議に感じるのはresult
変数への代入のコードです:
let result = (|| {
config = Some(Config::default()?);
let args: Vec<_> = try!(env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect());
callback(&args, config.as_ref().unwrap())
})();
これは、無名関数を作ってこれを即座に呼び出し、その結果を代入するという形になっています。Rustでは、ブロックそのものが式になれるため、このように無名関数の呼び出しにする必要はないのではないかという気もしてきます。しかしながら、単なるブロックではなく、一度無名関数でくるんでそれを呼び出している理由は、エラー処理の関係です5。
まずは、次の部分です:
config = Some(Config::default()?);
この?
演算子は、次のコードとほぼ等価な6シンタックスシュガーです:
config = Some(
match Config::default() {
Ok(v) => v,
Err(e) => return Err(e),
}
);
つまり、Config::default()
関数がエラーを返した場合の振る舞いが、無名関数とブロックでは異なるのです。無名関数の場合にはresult
変数にErr(error)
という値が代入され、process()
関数の残りの部分が実行されますが、ブロックの場合には即座にprocess()
関数から脱出しようとしてしまいます。これが、無名関数で包んである理由です。
?
演算子とtry!
マクロ
なお、?
演算子が導入される前から、try!
マクロを使って次のように書くことができました:
config = Some(try!(Config::default()));
このように、マクロでも実現できることではあるのですが、あまりにも使用頻度が多いのと、try!
マクロの入れ子が発生すると読みにくいコードになるため、言語機能として用意したというわけです。
しかしながら、今のところtry!
マクロを完全に取り除くこともできていないようで、この次の文はtry!
マクロを使って書かれています:
let args: Vec<_> = try!(env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect());
これは、次のように書き直しても良さそうなものです:
let args: Vec<_> = env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect()?;
ところが、まだ?
演算子の実装が不十分なのか、これでは型推論に失敗してしまうため、次のようにcollect()
関数の方に型パラメータを付けないとコンパイルが通りません:
let args = env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect::<CargoResult<Vec<_>>>()?;
これならばtry!
マクロの方がマシですね。まあ、そのうち直るんじゃないかとは思います。
ところで、私が一つ気になっている部分は、config
変数をmut
にして無名関数内で代入し、外に取り出していることです:
let mut config = None;
let result = (|| {
config = Some(Config::default()?);
...
let (result, config) = match (|| {
let config = Config::default()?;
let args: Vec<_> = try!(env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect());
Ok((callback(&args, &config)?, config))
})() {
Ok(s) => (Ok(s.0), Some(s.1)),
Err(e) => (Err(e), None),
};
とどのつまり、この無名関数が何をやっているか
エラー処理の話でだいぶ横道にそれた気がするので、元に戻します。process()
関数の処理の本体はこの無名関数部分で、残りは後処理なので、まずはこの無名関数を詳細に読んでいくことにします:
let result = (|| {
config = Some(Config::default()?);
let args: Vec<_> = try!(env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect());
callback(&args, config.as_ref().unwrap())
})();
この無名関数は、実行環境から引数や環境変数などを取得して、これを[String]
型の引数リストとConfig
型のコンフィグレーション情報の二つにまとめあげ、callback()
関数へと渡すという働きをしています。
先ほど引用したこの部分が、コンフィグレーションのデフォルト値を生成していることは明らかでしょう:
config = Some(Config::default()?);
Config
型やConfig::default()
関数の実装は次回見ることにします。
その次では、このプロセスに渡されたコマンドライン引数のリストをVecに変換しています:
let args: Vec<_> = try!(env::args_os().map(|s| {
s.into_string().map_err(|s| {
human(format!("invalid unicode in argument: {:?}", s))
})
}).collect());
std
crateのenv::args_os()
関数はArgsOs
という型のイテレータを返しますが、このイテレータで巡ることができるのは、要素としてOsString
型の値を保持している線形コンテナです。OsString
型は実行環境のネイティブな文字コードで保持された文字列で、そのままではRustの標準的なString
型とは互換性がありません。そのため、各要素をOsString::into_string()
関数でString
型へと変換する必要があります。
なお、このようにイテレータに対してIterator::map()
などのイテレータアダプタを利用してモナド的な演算パイプラインを構築し、最後にcollect()
関数を適用して別のコンテナを構築するのがRustでは常套手段となっているようです。なお、collect()
関数で最終的に構築されるコンテナとしては、FromIterator
トレイトを実装している任意のコンテナを用いることができるため、上記のように型アノテートでそれを指定しないと曖昧になってしまいます10 11。この辺の事情は例の本のイテレータの項を参照してください。
最後に、この二つの値config
とargs
をcallback
へと渡しています:
callback(&args, config.as_ref().unwrap())
このコールバックの中で、cargoの正味の処理が行われることになります。config
はOption<Config>
型でなおかつSome()であることが確実なので、as_ref()
関数でOption<&Config>
へと変換した後、unwrap()
関数で&Config
な参照を取り出しています。
process()
関数の残りの部分
上の無名関数の呼び出しの結果、result
変数に格納されているのはcallback
の戻り値か、その呼び出し前に発生したエラーです。この変数の型はcallbackの戻り値型であるCliResult<Option<V>>
となります。
残りの部分は、このresult
変数の内容を最終的に表示するためのコードです:
let mut verbose_shell = shell(Verbose, Auto);
let mut shell = config.as_ref().map(|s| s.shell());
let shell = shell.as_mut().map(|s| &mut **s).unwrap_or(&mut verbose_shell);
process_executed(result, shell)
}
pub fn process_executed<T>(result: CliResult<Option<T>>, shell: &mut MultiShell)
where T: Encodable
{
match result {
Err(e) => handle_error(e, shell),
Ok(Some(encodable)) => {
let encoded = json::encode(&encodable).unwrap();
println!("{}", encoded);
}
Ok(None) => {}
}
}
ここで登場しているshell
変数やその型であるMultiShell
は、その名前とは裏腹に、あまりシェルとは関係がなく、標準出力および標準エラー出力へのハンドルと、その属性などを保持しているデータ構造です。コードを書いた人の気持ちとしては、「呼び出し元のシェルによって決定された属性」というような意味合いなのかもしれません。
callback
の呼び出しの結果がエラーなら、handle_error()
関数を呼び出してそのエラーの内容を表示し、成功で何か情報を持っていれば、その内容をJSON形式の文字列に変換して表示します。成功で何も情報がなければ、無言のまま終了します。いずれにしても、この関数を抜けた後はmain()
関数の最後に到達し、cargoのプロセス自体が終了することになります。
handle_error()
関数を読むためには、後回しにしていたConfig
型やMultiShell
型についての知識が必要になるため、これまた次回にしましょう。
call_main_without_stdin()
関数
さて、もう一息。もう一度コールスタックを思い出してみましょう:
execute_main_without_stdin()
|
+--- process()
|
+--- ||() // 無名関数
|
+--- call_main_without_stdin()
|
+--- exec() // execクロージャとしてsrc/bin/cargo.rsのexecute()関数が渡されている
execute_main_without_stdin()
関数から呼び出されたprocess()
関数のclosure
引数として渡されているのは無名関数です:
pub fn execute_main_without_stdin<T, V>(
exec: fn(T, &Config) -> CliResult<Option<V>>,
options_first: bool,
usage: &str)
where V: Encodable, T: Decodable
{
process::<V, _>(|rest, config| {
call_main_without_stdin(exec, config, usage, rest, options_first)
});
}
ここからさらに呼び出されているcall_main_without_stdin()
関数はすぐ直後で定義されています:
pub fn call_main_without_stdin<T, V>(
exec: fn(T, &Config) -> CliResult<Option<V>>,
config: &Config,
usage: &str,
args: &[String],
options_first: bool) -> CliResult<Option<V>>
where V: Encodable, T: Decodable
{
let flags = flags_from_args::<T>(usage, args, options_first)?;
exec(flags, config)
}
この関数は、コマンドライン引数を解析して、その結果をexec
クロージャに渡しているだけの関数です。たったこれだけなんだから、何でexecute_main_without_stdin()
関数に直接書かないんだ、と思うかもしれませんが、よーくみるとpub
が付いていることが分かります。この関数は別のところからも呼ばれるので、独立した関数として書いてあるんですね。
ここでやっと、コマンドライン引数の処理をしていることが分かります。flags_from_args()
関数を見てみましょう:
fn flags_from_args<T>(usage: &str, args: &[String], options_first: bool) -> CliResult<T>
where T: Decodable
{
let docopt = Docopt::new(usage).unwrap()
.options_first(options_first)
.argv(args.iter().map(|s| &s[..]))
.help(true);
docopt.decode().map_err(|e| {
let code = if e.fatal() {1} else {0};
CliError::new(human(e.to_string()), code)
})
}
ここでは、docoptという外部のcrateを使ってコマンドライン引数処理を行っているようですね。env_loggerの時と同様にして正体を探ることができますが、その過程は省略します。
docopt
docoptはなかなか面白いライブラリで、usage文字列、すなわち、人間が読める形で書かれたコマンドの使用法からコマンドライン引数プロセッサを生成してくれます13。
この段階では14、usage
変数には次のような文字列が渡されています:
const USAGE: &'static str = "
Rust's package manager
Usage:
cargo <command> [<args>...]
cargo [options]
Options:
-h, --help Display this message
-V, --version Print version info and exit
--list List installed commands
--explain CODE Run `rustc --explain CODE`
-v, --verbose ... Use verbose output (-vv very verbose/build.rs output)
-q, --quiet No output printed to stdout
--color WHEN Coloring: auto, always, never
--frozen Require Cargo.lock and cache are up to date
--locked Require Cargo.lock is up to date
Some common cargo commands are (see all commands with --list):
build Compile the current project
clean Remove the target directory
doc Build this project's and its dependencies' documentation
new Create a new cargo project
init Create a new cargo project in an existing directory
run Build and execute src/main.rs
test Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this project to the registry
install Install a Rust binary
See 'cargo help <command>' for more information on a specific command.
";
Docopt::new()
関数がこのOptions
のところを解析し、コマンドライン引数を処理するdocopt::Parser
型の構文解析器を生成しています。その使い方は、上のコードを見ればほぼ明らかですね。詳しくはドキュメントを見るのが良いでしょう。
ちょっと脱線しますが、Rustでコマンドライン引数を処理するためのcrateはdocoptだけではありません。rustupコマンドが採用しているのはclapというcrateです。これはかなり高機能で、cargoが力業でやっているようなサブコマンドの処理(これは次回以降に見ます)ですらも素直に処理できます。しかしながら、その分「usageを書けば終わり」というわけには行かず、一つ一つ書いてやる必要があります15。
このあたりはアプリケーションの複雑さや作者の好みによって、どちらを使うのかを選ぶことになるのでしょう。
おわりに
というわけで、次回は今回後回しにしたConfig
型などを見ていくことにします。その後で(次々回かな)、execute_main_without_stdin()
関数の第一引数として渡されているexecute()
関数の中を見ていくことにしようと思います。
脚注
-
panicするのは気持ち悪いような気もしますが、この関数が失敗するようなケースでは、どのみちまともにリカバリできる気もしないので、わざわざ自前の関数を用意して真面目にエラーハンドリングをする必要もないのでしょう。いずれにしても、余計なコードパスを通らずに安全に終了することだけは保証されます。 ↩
-
とはいえ、さすがにこれはちょっと迂遠すぎる書き方だったようで、Rust 1.19.0のcargoではこんなコード(GitHub)になっており、はるかに普通の書き方になっています。 ↩
-
要するに、訳の分からないエラーメッセージが大量に出るアレのことです。コンセプトが入るとだいぶマシになりそう。 ↩
-
なお、最近のnightlyには
do catch
という構文が入ったようです。 ↩ -
「ほぼ等価」であって「等価」ではありません。コンパイルエラーになります。このあたりの事情は、例の本のエラーハンドリングの章を熟読してください。 ↩
-
あるいは、
process()
関数の残りの部分を書き直せば、CliResult<(Option<V>, Config)>>
型の変数一つで持ち回すことが可能かもしれません。が、所有権がらみでだいぶ面倒くさいことになる。 ↩ -
match
の代わりにコンビネータで.map(|s| (Ok(s.0), Some(s.1))).unwrap_or_else(|s| (Err(s), None))
としてもいいんですが、却って分かりにくいっすね。match
が中置にできればいいのだが。 ↩ -
無名関数ではなく、全部コンビネータでつないでいくという方法もありうるのですが、
args
をcollect()
するのに型注釈が必要になるせいで冗長になる上に、途中で明示的にCargoResult<_>
をCliResult<_>
に変換するor_else()
を入れないといけなくて、却って長くなります。 ↩ -
あるいは、先ほども書いたとおり、
collect()
関数の型パラメータとして戻り値型を明示的に付ける方法もありますが、このコードのように要素をResult型へマップするようなイテレータアダプタを含んでいる場合には冗長になりがちです。 ↩ -
イテレータアダプタとして、要素を
Result<T,E>
型へ変換するようなmap()
関数がcollect()
関数の直前に挿入されていると、collect()
関数の戻り値型はContainer<Result<T,E>>
ではなくResult<Container<T>, E>
型となり、各要素からResult
型が引き剥がされ、コンテナをResult
型で包む形となります。collect()
関数の変換の途中でエラーが発生すると、そこまでの繰り返しの結果は捨てられ、エラー値がそのままcollect()
関数の結果となります。 ↩ -
ところで、この
process()
関数の呼び出しに明示的に付けられた::<V, _>
という型パラメータがなぜ必要なのか良く分からないんですが(無くてもコンパイルが通る)、型推論が弱かったころの名残ですかねえ。 ↩ -
もともとはPythonの同名のパッケージが元になっているようですね。 ↩
-
上述した通り、
call_main_without_stdin()
関数は、ここで見ているコードパス以外のところからも呼ばれます。具体的には、src/bin/cargo.rs
のexecute()
関数でサブコマンドの処理を行う際に呼ばれるのですが、この時にはサブコマンドごとに固有のusageが渡されます。 ↩ -
clapの場合には、逆に、この記述から自動的にusageが生成されることが分かります。 ↩