Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

Rustでも遊びたい

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を渡すのはダメだぞと言われている。この長ったらしい型はFile::openが戻しているわけだが、要するにファイルをopenすると「ファイルポインタかIOエラーかどちらかが返るよ」ということを1つの型として表している。大胆不敵である。

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>である。入れ子になっているけど、そんなに大変なわけじゃない。ファイルはopen出来ないこともあれば、readしたらエラーになることも、空のこともあるのである。大変だ。

空なら、空文字列を印字すればいいような気がする。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!("")が実行されているからで、厳密には正しい動きとは言えない。これは直すべきだろう。