08 May 2017

Rust week

GW は引き篭もって Rust で小さなプログラムを書くようにしていたので, その時に感じたことをまとめておく.

環境構築

Atom (linter-rust) とターミナルで.

選ぶモチベーション

Rust を学ぶモチベーションは, 自分の中で C++ の代替を目指したため.

書いてみて

パッケージ管理システム最高

玉石混交だけど.

デバッガがついてくる

rust-lldb, rust-gdb がついてきた.

省略の文化

すごい適当な気分でプログラミングしていると, とにかくコンパイラに怒られる. 最初に面食らったのは, ファイルの読み書きを書こうと思ってサンプルコードをコピーして動かしてみるとコンパイルエラーになる.

https://doc.rust-lang.org/1.1.0/std/fs/struct.File.html

use std::io::prelude::*;
use std::fs::File;

let mut f = try!(File::create("foo.txt"));
try!(f.write_all(b"Hello, world!"));

let mut f = try!(File::open("foo.txt"));
let mut s = String::new();
try!(f.read_to_string(&mut s));
assert_eq!(s, "Hello, world!");

これが example とあるので, これを適当にコピーして貼っつけても動かない.

fn main() {
    use std::io::prelude::*;
    use std::fs::File;

    fn foo() -> std::io::Result<()> {
      let mut f = try!(File::create("foo.txt"));
      try!(f.write_all(b"Hello, world!"));

      let mut f = try!(File::open("foo.txt"));
      let mut s = String::new();
      try!(f.read_to_string(&mut s));
      assert_eq!(s, "Hello, world!");
      Ok(())
    }
}

サンプルコード部分に playground へのリンクがあり, 飛んでみると上の様になり, これだと問題ない.

単純な話で, try! マクロは内部で return 文を展開するので try! マクロが返そうと する戻り値の型と関数の戻り値の型が一致しないとコンパイルエラーになる. 最初はマクロを理解しておらず, 最初の example とにらめっこしたまま無駄に時間を消費した.

提供されているライブラリの多くの関数は値を Option<T> もしくは Result<T,E> で返してくるため, 素直に取り出そうとするとパターンマッチして分岐を記述する必要がある. が, パターンマッチが面倒くさい人のために unwrap メソッドが用意されており Option<T>, Result<T,E> から 内部の値を取り出すことが出来る.

ただし unwrapOption<T>Some(v), Result<T,E>Ok(v) であった場合のみ正常に動作し, NoneErr(err) などであった場合には panic になってしまい, プログラムが不正に停止してしまう.

そこで try! では Result<T,E> 型の変数に対して, unwrap の様に展開を試みて, 失敗するようなら return Err(E) をするというマクロになっている. この動作は, ふら~っとやってきて「ファイル読み書きサンプルコードは?」という心構えの初学者に理解させるのは少し難しめだと感じた.

Option<T> の場合でも Result<T,E> に変換出来るので, 同様に try! で処理出来てしまう. さらに省略記法は進み, 今は try! マクロではなく, Result<T,E> を返す式の末尾に ? をつけることで同じ動作になっている. 更に短く.

省略はそれだけではなく, 型名や lifetime パラメータなど様々あるが, この try! のようなマクロを作れる環境が省略に一番寄与しているように感じる. いくつかパッケージをのぞいて見ても, パッケージ毎に独自のマクロを定義して省略が行われている様子が見て取れる.

引数が move値渡し or 参照渡し

わかりやすい. 引数のコピーコストはない.

Option<T> とか Result<T,E> の内部値を取り出して操作して再び包むような操作を map メソッドで行うが, その際のコピーコストもない(はず).

const と書かなくて良い

最高. mutable な方に対して mut と書かなくてはならない.

文字列・配列の扱い

これも簡単だけど最初に面食らったので. String は公式ドキュメントだと growable string とあり, 可変長の文字列を表す primitive クラス. それとは別に str というクラスもあり, これは固定長の primitive クラス.

growable 配列は Vec<T> で, 固定長なものはスライスと呼ばれて let a = [1,2,3]; みたいに定義する.

エラーハンドリングを強制する

最初の try! マクロについても触れたように, とにかく標準ライブラリでは Option やら Result に包んで返してくれるので ? 構文なりを使わず unwrap で取り出していくと危険なプログラムが出来あがる.

なので, unwrap しているところはエラーハンドリングをおざなりにしていることの証でもあるので, もしあとで綺麗に直すつもりがあるのなら目印になる.

https://doc.rust-lang.org/book/error-handling.html

公式でもエラーハンドリングの章は割りと長め. でもここが読めたら割りと達成感がある.

おわりに

なんだかんだ, まだまだ理解が足りてないので深めていきたい.

今のところ, 簡単なスクリプト用途などでは Ruby を使う方に習熟度として軍配が上がっているので, その代替になるくらい理解していきたい.

lifetime パラメータの使い所がまだ分からない程度には習熟していない.