rust初心者へのガイド
シルバーウィークの進捗が芳しくなかったので雑な記事書いてお茶を濁しとく。rustをそれなりに(といっても1000行くらい)書いて溜まった知見をとりあえず出す。rust1.3時点。
最初の方で熱く語ってるが多くの人にとって欲しい情報は下の方にあると思う。
どんな言語
公式から持ってくるとこんな感じ。
- zero-cost abstractions
- C++くらいの性能と思えばいい。
- move semantics
- 他にはない難しい概念。しかしこれのおかげで様々な機能を教授出来る。
- guaranteed memory safety
- move semanticsにより安全でない操作はコンパイル時に弾ける。
- threads without data races
- move semanticsその他により安全でない操作は(ry
- trait-based generics
- 継承ベースとは違って開いている。型を定義した後に機能を追加出来る。
- pattern matching
- 便利だよねー。
- type inference
- (超)重量級言語ながらタイプ数は少なめ。
- minimal runtime
- move semanticsのお陰でGCがないので本当に小さい。hello, worldが277KBだった。
- efficient C bindings
- ABI的に良い感じなのでブリッジングコストがほぼない。
さて、ここでは足りないことを書いておくと、現在Mozillaで開発されている言語で、LLVMバックエンドのネイティブコンパイル言語。LLVMにトラウマがある人もバイナリ配布されているので気軽に使える。 rustを使った大きなプロジェクトはレンダリングエンジンのServoがある。
レンダリングエンジンに使われているだけあって対応OS/アーキテクチャは広く、iOSやAndroidでも動く。C FFIもC APIもある。
コミュニティは非常に活発で、インフラやライブラリは一通り揃った感じはする。マイナー言語を見てきた身としては1000ライラリくらい集まると一通りのことは出来るようになるかな、と思っているがrustは若い言語ながら3000ある。増えるペースも速いので欲しいものはまずあると思っていい。
長らくAPIの破壊的変更をガンガンやる時期があって、1ヶ月前のhello worldが動かないとかもザラだったが2015年5月に1.0が出て以来見違えたように大人しくなって人が増え始めている。APIについてはunstable, stableだとかのラベルを付けるようになったので安心して使える。
開発フローについてはFirefoxと同じく6週間毎に上流から新しいバージョンが降ってくる。ので今はもう1.3が出ている。1.0から1.3はコンパイル/実行時のパフォーマンス改善が主。新しい機能はPythonのPEPみたいにRFCで管理している。
所有権や生存期間など新しい概念を導入していてとっつきにくいがこれらの概念のお陰で並列化しても安全だし、何よりメモリ管理を静的に解決出来るのでGCがなくてパフォーマンスが出るわ安定するわStop the Worldがないわで非常に良い言語。個人的にはデータベースとかのパフォーマンスと信頼性が必要なミドルウェアを書くのに向いてるのではと思っている。
何指向言語かと言われると難しい。安全指向?継承はないがオブジェクト指向といえばそうだし函数型っぽくなくもない。並行を意識して作ってあるから並行指向と言えなくもない。継承を止めたC++のような所有権と副作用を入れたHaskellのような言語。副作用はバリバリ使うのであまり函数型言語として見ない方が幸せになれると思っている。
traitがどんなものかというとHaskellの型クラスに(多分)同じ。しかもHaskellのderiving Show
みたいに#[derive(Debug)]
とかも書ける。便利。
生存期間と所有権がどういうものかというと、
let foo = Foo::new(1);
println!("{:?}", foo);
let foo = Foo::new(2);
println!("{:?}", foo);
でfoo
をFoo::new(2)
にバインドした時、Foo::new(1)
は所有者が居なくなるのでその時点で開放される。GCと違う点は、GCはその時点ではゴミになるだけで、次にGCが走った時にようやく開放されるが、rustはその場で開放される、free
を自動で挟む。そうなると、コンパイラは最適化で同じサイズをfree
してまたアロケートするのを同じ領域を使うようにする筈だ。これで領域の節約とかアロケーションコストの節約の他に、「今使った」メモリを再利用出来るのでキャッシュに載ったままメモリを使える。ここまでの効率化を「自動で」やってくれるのはrustだけではないかと思っている。
このように素晴しい言語機能があってコミュニティも活発で安定した言語なので流行ればいいなと思っている。とはいっても気軽に書ける言語ではないのであらゆる所で使われる言語とは思っていない。先に言ったようにデータベースとかのパフォーマンスと信頼性が必要なミドルウェアを書くのに向いてるのではと思っている。goがnext Cならrustはnext C++かな、と。
rustは難しい。学習曲線が急峻だ。しかし手を動かしてその急峻な崖を乗り越えるだけの価値はある言語だと思うので是非試してみて欲しい。
さて、情報セクションだ。
ドキュメント
入門
trplと略されるThe Rust Programming Languageを読むととりあえず基本的な概念を一通り学習出来る。
書き始めた
文法とかをサクっと確認したいならThe Rust Referenceがある。
標準ライブラリを調べたいならAPIドキュメントがある。一見分かりづらいが一番上に検索窓があるので全体検索が出来る。
コード例が欲しいならRust by Exampleがある。
軽く試す
Rust Playgroundを使えばWeb上で試せる。質問とか投げる時にサンプルコードをここに載せて渡すと捗る。
開発環境
コンパイラ
公式から簡単にバイナリ落としてこれる。Macだとbrewでも入った気がする。FreeBSDだとpkgで入る。Debianのパッケージも出来たらしい(9/23)がUbuntuにはまだ(9/23)きてない。すぐ来るだろう。
しかし後述のracerのためにソースが必要なので別途ソースはダウンロードする必要がある。
エディタ
Emacs, Vim, Atomだったらracerを使う。 racerのソースを持ってきてコンパイルしてエディタプラグインをエディタに入れてrustコンパイラのソース持ってきて2行設定書けば使える。ソース補完と定義元ジャンプがある。ちゃんと型を見て補完候補出してくれるし標準ライブラリのソースにもジャンプ出来るので中々便利。
gofmtのrust版、rustfmtは開発版のコンパイラを持ってこないとコンパイル出来ないので私は諦めているが使いたい人は試すといいと思う。少なくともEmacs向けのプラグインはある。
ビルドツール
コンパイラと一緒に配布される(FreeBSDのpkgでは別になってる)Cagroがある。雛形作成、依存解決、ビルド、テスト、ベンチマークなどのタスクが出来る。クロスコンパイルとかも。
パッケージ管理
クライアント側はCargo。セントラルレポジトリ的なのはcrates.io。crates.ioに登録されてなくてもCargoはgitから取ってくるとかも出来るので野良パッケージも使える。
テスト
関数に#[test]
アノテーションを付ければ良い。つまり、ソースとテストを同じファイルに書ける。結構便利。テスト用ビルドでのみコンパイルされて他のビルドだと無視される(と思う。)。
fn fib(n:isize) -> isize{
if n < 2 {
1
}
else {
fib(n - 1) + fib(n - 2)
}
}
#[test]
fn test_fib(){
assert(fib(1) == 1);
}
これ便利とかここ躓いたとか。
所有権
分かってたけどやっぱり躓いた。局所的には「あ、ここ所有権必要だわ」とか分かるのだが大域的には難しい。
例えばボトムアップで作っていくと、小さな関数で所有権が必要だがそれを呼び出そうとしたら呼出元が所有権を持っていなくて困るとか。小さな値とか状態を持たない値だったらclone
して渡すのだがそうでなければ手戻りが発生する。この辺は実際に書いて経験を積むしかなさそう。
因みに代数的データ型と所有権でも困っている。
enum Value {
Str(String),
Int(isize)
}
とかするとStr
データコンストラクタがStringの所有権を持っているのでパターンマッチで取り出す時に所有権が貰えず、match{Value::Str(ref str) => ...,}
と、ref
を使って借りるしかない。
まだ経験が足りないので困ったまま。
サイズ
rustはコンパイル時にメモリ管理を決定するのでコンパイル時にデータのメモリサイズが決まってないといけない。例えば以下のコードはコンパイルが通らない。Bazにおいて、fooのサイズが決定出来ないと言われる。
trait Foo {
}
struct Bar {
}
impl Foo for Bar {
}
struct Baz {
foo: Foo
}
これはFoo
はただのインターフェースの定義であって、データを定義してないので実際にFooを実装したデータ型のサイズが分からないからだ(今後変更がある模様。)。次のようにパラメータにすれば解決出来る。
struct Baz<T> {
foo: T
}
impl <T:Foo> Baz<T>{
}
因みに元の定義とは変わってデータの時点ではTで、implを書く時にFooに絞っているのは不要な所では不要な条件を付けないようにしているからだろうか。 変な値を入れられて困りそうだが、構造体のフィールドを公開しなければ勝手に値が作られることはなく、impl内に書いたコンストラクタを通してのみ値が作られるので心配無用である。
&[T]
とVec<T>
, &str
とString
使い分けはRustの文字列のガイド - Qiitaを見て欲しいが、相互変換で困ることがあったので。
String
から&str
に変換する時に「as_slice
はunstable」と言われる。こうしてやれば良いようだ。
let string = "String".to_string();
let lent_str = &string[..]
Vectorも同じ。
let vector = vec![1, 2, 3];
let lent_slice = &vector[..]
HashMap
所有権周りで困る。例えば次のコードはコンパイルが通らない。
match hash.get(key) {
Some(v) => v,
None => {hash.insert(key, default); default}
}
matchの中でhashがborrowされてると判断されるのでNone節でhashにinsert出来ない。ワークアラウンド もあるが、どう考えてもイケてないので改善される模様
データ型と参照
データ型の中で参照を使いづらい。
struct Value {
Str(&str),
Int(isize)
}
とすると、怒られる。&str
は自分の物ではないので生存期間が分からないからパラメータで受け取らないといけない。
struct <'a>Value<'a> {
Str(&'a str),
Int(isize)
}
そして、やはり所有権を持っていないので次のようなメソッドを定義出来ない。
impl <'a>Value<'a> {
fn empty_str() -> <'a> {
Value::Str("")
}
}
…と思ったらなんか出来ちゃった。今まで使い方が悪かったのかも。これはナシ。
モナド
mdoというdo記法っぽく書けるマクロがあるがクロージャを作るとそのクロージャが変数の所有権を持っていって面倒だったのでそんなに良くなかった。optionモナドに関しては素直にmapとandThenを使った方が良い。
try!
rustのコードでは至る所でResult(Either)型が返ってくる。それに対して毎回パターンマッチするのはやってられない。かといって安全でないunwrap()
を各所で使うのも精神衛生に良くない。Errに対してunwrapを使うとpanicになるが、rustにはpanicをハンドルする方法はない。
そこでtry!
。返り値がErrだったらそのままErrで関数から抜け、Okだったらその値を返すマクロ。多分展開結果はこんな形になってる:
let v = try!(foo());
が
let v = match foo() {
Ok(v) => v,
e @ Err(_) => return e
};
。
これの逆、成功したらその値で抜け、ErrだったらErrを返して処理を継続するやつとかオプション版とかも欲しい。
最後に
Lisp処理系作ろうとしたけど完成しなかったのでそっとここに置いときますね