https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/README.html
を一通り読んでみた。うむ、わからん。
日々のツールを作ってみながら覚えるのがよろしかろう。
まず、ファイルIOが使えないとつまらないので、テキストファイルの最後の1行を出力するようなものを作ってみよう。
tail -n1 hoge.txt
と同じ動作をするものだ。
とりあえず、Rubyで書くと
#!/usr/bin/ruby lines = File.readlines("hoge.txt") print lines.last
の2行である(tailコマンドと内部的な動作は全然違うけど、巨大なファイルを食わせなければ見た目は同じである)。ARGVを使わずにファイル名をコードに書いてしまっているのは、Rustでコマンドライン引数をどう扱うのかは次の課題とするからだ。
リファレンスのstd::ioの辺りを見ると、readlines相当はあるし、イテレータにlastというメソッドもある。サンプルコードを参考にしてそのまま書き写してみよう。
use std::io::prelude::*; use std::io::BufReader; use std::fs::File; fn main() { let f = File::open("hoge.txt"); let reader = BufReader::new(f); let lines = reader.lines(); println!("{}", lines.last()); }
もちろんエラーになる。Rustは低水準を扱うにもかかわらず、コンパイルが通ったらまず間違いなくメモリ操作を失敗しない、いわば厳しいC言語だ。だから、そう易々とコンパイルは通らない。
> cargo run Compiling tail1_rust v0.1.0 (file:///Users/tambara/rust/tail1_rust) error[E0277]: the trait bound `std::result::Result<std::fs::File, std::io::Error>: std::io::Read` is not satisfied --> src/main.rs:7:18 | 7 | let reader = BufReader::new(f); | ^^^^^^^^^^^^^^ the trait `std::io::Read` is not implemented for `std::result::Result<std::fs::File, std::io::Error>` | = note: required by `<std::io::BufReader<R>>::new` error: no method named `lines` found for type `std::io::BufReader<std::result::Result<std::fs::File, std::io::Error>>` in the current scope --> src/main.rs:8:24 | 8 | let lines = reader.lines(); | ^^^^^ | = note: the method `lines` exists but the following trait bounds were not satisfied: `std::result::Result<std::fs::File, std::io::Error> : std::io::Read`, `std::io::BufReader<std::result::Result<std::fs::File, std::io::Error>> : std::io::BufRead` error: aborting due to 2 previous errors error: Could not compile `tail1_rust`. To learn more, run the command again with --verbose.
Rustのコンパイルエラーは近年まれに見るレベルの親切さだと思うが、だからといってどうしたらよいかわかるかは別の問題である。
7行目は、要するにBufReader::new()の引数にstd::result::Result
std::ioのリファレンスを読むと、こういうときにはtry!マクロでくくってしまえば良いように思える。こんな感じだ。
fn main() { let f = try!(File::open("hoge.txt")); let reader = BufReader::new(f); let lines = reader.lines(); println!("{}", lines.last()); }
しかしながら、上手く行かない。こういうエラーに変わる。
Compiling tail1_rust v0.1.0 (file:///Users/tambara/rust/tail1_rust) error[E0308]: mismatched types --> src/main.rs:6:13 | 6 | let f = try!(File::open("hoge.txt")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected (), found enum `std::result::Result` | = note: expected type `()` found type `std::result::Result<_, _>` = help: here are some functions which might fulfill your needs: - .unwrap() - .unwrap_err() - .unwrap_or_default() = note: this error originates in a macro outside of the current crate
「()を期待しているのに、Resultになってますけど?!」とおっしゃってる。空のタプルを期待しているのは誰だ?
ひとしきり悩んでググってした結果、とあるサイトで「これはmain関数が戻り値なし(の場合、Rustは空タプルを戻すってことらしい)じゃないといけないのに、try!マクロの中にはエラーの時にreturnするようになっているのが原因」と書いてあった。なるほど。この単純なスクリプトの場合、ファイルが開けなければ落ちてもらって構わない。その時には、エラーメッセージが親切にも教えてくれているように、unwrapしてしまえば良い。
fn main() { let f = File::open("hoge.txt").unwrap(); let reader = BufReader::new(f); let lines = reader.lines(); println!("{}", lines.last()); }
さて、次のエラーに取りかかろう。
> cargo run Compiling tail1_rust v0.1.0 (file:///Users/tambara/rust/tail1_rust) error[E0277]: the trait bound `std::option::Option<std::result::Result<std::string::String, std::io::Error>>: std::fmt::Display` is not satisfied --> src/main.rs:10:20 | 10 | println!("{}", lines.last()); | ^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `std::option::Option<std::result::Result<std::string::String, std::io::Error>>` | = note: `std::option::Option<std::result::Result<std::string::String, std::io::Error>>` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string = note: required by `std::fmt::Display::fmt` error: aborting due to previous error error: Could not compile `tail1_rust`. To learn more, run the command again with --verbose.
なんと、last()が戻している型はstd::option::Option
空なら、空文字列を印字すればいいような気がする。Optionだけ外してみよう。
fn main() { let f = File::open("hoge.txt").unwrap(); let reader = BufReader::new(f); let lines = reader.lines(); println!("{}", lines.last().unwrap_or("")); }
安易すぎる。そんなに甘くないので、以下のエラーになる。
error[E0308]: mismatched types --> src/main.rs:10:43 | 10 | println!("{}", lines.last().unwrap_or("")); | ^^ expected enum `std::result::Result`, found reference | = note: expected type `std::result::Result<std::string::String, std::io::Error>` found type `&'static str`
OptionがSome()の場合にはResultになって、Noneの場合には””(型は&'static str)になるんだから怒られるわけである。こうすればいいのかな?
fn main() { let f = File::open("hoge.txt").unwrap(); let reader = BufReader::new(f); let lines = reader.lines(); println!("{}", lines.last().unwrap_or(Ok("".to_string()))); }
すると、Optionはちゃんと外れたようなエラーに変わる。
> cargo run Compiling tail1_rust v0.1.0 (file:///Users/tambara/rust/tail1_rust) error[E0277]: the trait bound `std::result::Result<std::string::String, std::io::Error>: std::fmt::Display` is not satisfied --> src/main.rs:10:20 | 10 | println!("{}", lines.last().unwrap_or(Ok("".to_string()))); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `std::result::Result<std::string::String, std::io::Error>` | = note: `std::result::Result<std::string::String, std::io::Error>` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string = note: required by `std::fmt::Display::fmt` error: aborting due to previous error error: Could not compile `tail1_rust`. To learn more, run the command again with --verbose.
読み取りエラーは落っこちて構わない気がするので、もういちどunwrapしておく。
fn main() { let f = File::open("hoge.txt").unwrap(); let reader = BufReader::new(f); let lines = reader.lines(); println!("{}", lines.last().unwrap_or(Ok("".to_string())).unwrap()); }
ちゃんと動いたっぽい。
> cat hoge.txt 1行目 2行目 3行目 > ./tail1.rb 3行目 > cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/tail1_rust` 3行目 > cat /dev/null > hoge.txt > cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/tail1_rust` > rm hoge.txt > cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/tail1_rust` thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/libcore/result.rs:868 note: Run with `RUST_BACKTRACE=1` for a backtrace.
ファイルを消したらパニックで落ちているが、エラーの中身もダンプされていてファイルが見つからなかったんだろうなと言うことはわかる。手元でちょっとしたツールを作るぐらいなら十分である。
ファイルが空の時に、改行だけ出してしまっているのはもちろんprintln!("")が実行されているからで、厳密には正しい動きとは言えない。これは直すべきだろう。