underscore-rust作ってみた。あとrust書いてみた動機とか
underscore-rust
underscore-rust 作った。名前から分かる通り、underscore.js の機能を rust にしたもの。
ライブラリ自体のドキュメントは上記に入れたつもりなので、必要であれば参照して欲しい。ポイントとしてはオリジナルの underscore.js のように _
の構造体みたいなものを用意するのではなく、標準の struct(Vec
, TreeMap
, HashMap
)にそのまま underscore 系の関数を追加した点。use すれば普通の API と同じ感覚で使うことができる。
作ったみたが、インターフェイスについては結構後悔している部分が多い。例えば HashMap の拡張は
fn invert(self) -> HashMap<V, K>;
とかにしてしまったけどテストしづらかったとかいう理由で ownership 取ってしまったので(ownership については後述)そこに関しては
fn invert(&'a self) -> HashMap<&'a V, &'a K>;
とかにすべきだったかなと思っている。まぁ、同じ型のものが返るので不都合はなさそうだしこっち方が扱いやすくていいだろと考えたのでこういうインターフェイスにしたのだが、オーバーヘッドが大きくなってしまうのは事実だ。所詮初学者が作ったものなので見る人から見ればおかしい部分が多分に含まれているはずで、その辺りの指摘は甘んじて受けていきたい。
作った動機
特に underscore-rust を作ってみたい実用的な用事があったわけではなく、単純に rust を書いてみたくて、教材として便利だったので作ってみた。underscore系のライブラリは、一体何番煎じか知らないが、とりあえず新しい言語を覚える際には良い教材だと思う。難易度は低いし、割と有益なものを作れる。javascript を引き合いに両者の違いを感じることができる点も良い。
自分は普段 LL しか使わない完全なゆとりエンジニアだが、rust はそういった層により低レイヤーを学ばせる用途に向いている。LL とかそれより上流のコミュニケーション一式が主戦場であっても、パフォーマンスチューニングだとかインフラの冗長化を見積もったりする段階で低レイヤーにまで手を伸ばさなければならない場合は多い。ライブラリの選定で知識が必要な時もある。あるいはもっと単純に、低いレイヤーのことを知るのは物事の道理を学ぶにあたって有益だと思う。 最近はインフラ層の技術がより LL がメインの人たちに近づいてきた印象があり、自分もそういうものと付き合っていかなければないので、この辺りで一度レイヤーが低いっぽい言語に触れておきたかった。
rust に手を出してみたきっかけはこういった気持ちがあったからだけど、これより前は同じような理由で go を触ってみたりしていた。ただ、GC が支配的な世界観でなんとなく違う気がして離れてしまった。rust のメモリ管理は基本人間が頑張るのでそういうのに惹かれたんだと思う。
こう書くと C とか C++ とかやればいいという完全に真当なことを言われそうなのだが、何か一つ知らない言語を選べといわれたらそれなりに新しいものを選びたくなる程度にはミーハーなので正直やる気がおきなかった。あとかなり前に読んだ「ふつうのプログラマのための haskwell〜」とか「7つの言語〜」とかで関数型言語は確かに綺麗だなあという気持ちがチラついたのもある。純粋な興味だけで言ったら関数型言語をガッツリやってみたかったのだが、いくらかでも目の前の課題とか明日のメシと地続きにする知識を得ようとしたら、関数型っぽい風味のある rust に手を出すというのは一つの着地点としてアリだった。
rust のちょっといい話
※この項は特に間違えている可能性が多く含まれるので気をつけて下さい
ポジショントークだけだとさすがにアレなので、具体的に rust のちょっといい話を考えてみた。例えば C でこんなコードを書いたとする。
struct hoge { int num; }; int* pick_num(struct hoge *h) { int *copy = &h->num; free(&h); return copy; } int main(void) { struct hoge *x; x = (struct hoge *) malloc (sizeof(struct hoge)); x->num = 5; int *y = pick_num(x); printf("%i\n", x->num); printf("%i\n", *y); return 0; }
上記のコードだと y
は free された値を参照してしまうダンジリングポインタとなる。これはコンパイルは通るが、実行すると落ちる。このコードを rust に置き換えてみる。
struct Hoge { num: int } fn pick_num(x: Box<Hoge>) -> int { x.num } // rust の場合、ブロックを抜けるときに常にメモリ解放されるので RAII は意識しなくていい fn main() { let x = box Hoge { num: 5i }; let y = pick_num(x); println!("{}", x.num); println!("{}", y); }
これをコンパイルしようとすると、以下のようにエラーを吐いてそもそもコンパイルできない。
hoge.rs:13:20: 13:25 error: use of moved value: `x.num` hoge.rs:13 println!("{}", x.num); ^~~~~ note: in expansion of format_args! <std macros>:2:23: 2:77 note: expansion site <std macros>:1:1: 3:2 note: in expansion of println! hoge.rs:13:5: 13:27 note: expansion site hoge.rs:11:22: 11:23 note: `x` moved here because it has type `Box<Hoge>`, which is non-copyable (perhaps you meant to use clone()?) hoge.rs:11 let y = pick_num(x); ^ error: aborting due to previous error
例に挙げた C のコードが阿呆すぎるような気もするが、とにかく、このようにコンパイルの段階で危険なポインタを教えてくれるのが rust の利点の一つだ。
これを支えているのは ownership,borrow,lifetime という概念で、以下に非常に雑な説明をすると
- 全ての変数に ownership が設けられる
- ownership を持つものだけがその変数へのアクセス権を持つ
- 変数の ownership は borrow することができる
- borrower は ownership を取らずに変数を使用できる
- ただし、borrower はメモリ解放や値の書き込みを行うことができない
- borrow には lifetime が設けられる
- コンパイラが自動で付加してくれるので普段はあまり意識しなくてよいが、参照を返す際などは明示的に lifetime を宣言して返したりする
とかになるが、ownership,borrow,lifetime は変数が mutable だった場合の挙動とかが少しずつ違ったりして一口に説明するのが難しいので、できれば公式ドキュメントの ここ とか ここ を読んでみて欲しい。
今回の例だと、let x
の ownership を pick_num
関数に移譲してしまっている。そのため、再度 x
にアクセスしようとしてもできない。こういうコードの場合、pick_num
の引数は単に borrow してくるのが普通である(はず)。
参考になったもの
rust を学ぶ上で役に立ったものを挙げてみる。
- http://doc.rust-lang.org/
- やはり公式ドキュメント。基礎がしっかりある人なら1日あれば読めるかもしれない。自分はかなり時間がかかった。
- http://rustbyexample.com/
- tour of go みたいなもの。tour of go ほど体系立ってるわけではないが、公式ドキュメントからの概念的な説明と並行しながら見るといいと思う。
- http://doc.rust-lang.org/nightly/std/(https://github.com/rust-lang/rust)
- 実際に rust で何か作っていこうとなったとき、rust way がわからない。そんな時に組み込みAPIの実装を読めて強力。一般に流通している rust ライブラリよりも綺麗に設計されている。
- http://rustforrubyists.com/
- rubyist 向けの rust 導入記事。今思うと大したこと書いてない気がするのだが、導入には良さそう。何より自分のようなゆとりでも門戸が開かれている事実が嬉しかった。