ちょっとRustの勉強がてらなんか作ってみようと思ったが、流石に現状のRustでいきなり本格的なものを作るのはリスキーというか無謀なので、まあ簡単なコマンドラインシェルのベースのようなものを作った。イメージとしてはPythonのcmdのショボい版。面倒だったんで補完とかは全然作ってない。とりあえず現状のソース。
使い方は同じディレクトリのmain.rsを見ての通りで、こんな感じ。
extern crate shell;
use std::collections::HashMap;
use shell::{Shell, Cmd};
struct S;
impl Cmd for S {
fn help(&self) -> &str {
return "test command";
}
fn exec(&self, arg:&str) -> bool {
print!("Entered: test {}\n", arg);
return false
}
}
fn main() {
let mut cb = HashMap::new();
let c = S;
let cmd = &(box c as Box<Cmd>);
cb.insert("test", cmd);
let s = Shell::new("prompt> ", cb, None, &[], "");
s.main_loop();
}
まだちょっとRustの流儀に慣れていないが、いくつかハマったところというか最初ちょっとギョッとした所を。
これは下記の2つの記事が参考になった。
要するに&strはビューであり、ある処理がその処理対象を所有している必要がない場合はできるだけビューの方を使い、逆に所有している(=オブジェクトのlifetimeを管理している)場合はStringやVec<T>を使った方が良いようだ。
ちょっとコードを書いた限りでは、lifetimeが複雑化したり、あるいはどうしてもlifetimeの関係でコンパイルすらできないような状態になった場合、スライスだけを使おうとしていてどこにもそのオーナーが存在しなかったり、あるいはスライス自体を無理やり自分の所有物にしようとしている事が多そうに感じた。
気がついたらRustのコアからはGCが取っ払われてライブラリ行きになっており、代わりにオブジェクトのlifetimeを指定するようになった。これのおかげで関数のローカルでVecに確保した値をオブジェクトの初期化子にスライスとして渡し、そのオブジェクトを関数から返すみたいなコードが書けなくてアレだったが、これは前述の通りオブジェクトのlifetimeを考えれば当然の事だったかもしれない。ちなみに最初に考えていたのは次のようなコード。(細部は省略)
pub struct Shell<'a> {
prompt: &'a str,
callback: HashMap<&'a str, &'a Box<Cmd+'a>>,
default: Option<&'a Box<Cmd+'a>>,
exit_commands: &'a [&'a str]
}
impl<'a> Shell<'a> {
pub fn new(p: &'a str, cb: HashMap<&'a str, &'a Box<Cmd + 'a>>, d: Option<&'a Box<Cmd + 'a>>, e: &'a [&'a str]) -> Shell<'a> {
let mut ec = Vec::new();
for a in e.iter() {
ec.push(a.to_string());
}
if ec.len() < 1 {
ec.push("quit".to_string());
}
return Shell{prompt: p, callback:cb, default:d, exit_commands: ec.as_slice()}; // エラー。ecはnew内のlifetime
}
}
そしてecをboxにしてヒープ上にアロケートしようとかいろいろやってみたが、最終的には前述の通りexit_commandsのオーナーは誰であるべきかを考えた結果、これスライスじゃなくてVecであるべきなんじゃないのと思ってこうなった。
とはいえ、この辺はまだ全然自信がない。
Rustはオブジェクトのサイズをコンパイル時に知る必要があるので、サイズが不定なTraitはそのままでは不都合がある模様。なので、Boxで間接参照の形にしてやる必要がある。まあ、これはCで再帰的な構造体を作るときにポインタを使うのと同じような事情だからハマったわけではないか。というか実際、Rustも再帰的な構造体を作るときもBoxで参照の形にしないといけない。
現状の問題は、まだまだ情報が少ないことと、言語の変化が激しすぎること。何しろNighly Build落としてきたら標準ライブラリのレイアウトが変わっていたとかが普通にあるのでなあ。
言語の目指している方向性そのものはかなり支持できるものではある。
晴れる屋でプロツアーM16のPPTQがやっていたので行ってきた。競技レベルのイベントは実はこれが初めてで、まあルール適用度の高いイベントに慣れる意味でも。デッキはアブザンの隆盛積んだアブザンアグロ。
1マナ域7枚、2マナ域10枚、3マナ域7枚、4マナ域4枚と生物を大量に採用し、マナカーブ通りに展開した後に隆盛を張ってゲームセットにする殺る気満々のレシピ。まあ、マルドゥに死ぬほど不利という欠陥があるが、そこはもうマルドゥを踏まないことを祈ってどうにかしようとした。というかアブザンでマルドゥを乗り越えようとしたらアグロ寄りの調整は多分ダメで、ミッドレンジでも除去の山がキツく、アブザンコントロールぐらい思い切って長期戦を見据えないと勝てなさそうだ。
というわけでスイスドロー8回戦の結果。
3-5で惨敗。開幕2敗で目なしではあったが、しかしアブザン同型を事故2回で落としてるのが痛すぎた。そこからはもう坂道を転げるように連敗。
そんでまあプレイミスで負けてるのとメタの読み違いとは俺のせいなのでそれは納得だが、しかし落としたゲームを見ると
という形。こうして見ると敗因の30%ほどが事故、それもマナフラッドとマナスクリュー。何か最近マジで事故で負けまくってる。色事故とかタップイン多すぎでテンポロスして負けてるだったら、まあ3色アグロというコンセプトに無理があるという事になるのだが、フラッドとスクリューはそれとは別だからなあ。ちょっとこのフラッドとスクリューでの敗北率については何らかの統計データが欲しい気がする。
あと今日の構築ミスとしては、赤単やトークン系を警戒しすぎた結果、中型〜大型クリーチャーをどんどん展開してくる緑単信心やステロイド対策を積めなかったことが挙げられるか。またマルドゥにはどうやっても構造上勝てないので、その点でもキツいかな。今のところ不利な緑単信心はサイドボーディングで戦えるし、緑黒星座も勝てなくもない。しかしマルドゥは本当にゲロ吐くぐらい不利なので、マルドゥがTier 1にいる現状でこれを使い続けるのは茨の道かもしれん。案外、ポジション的には白青英雄と同じかもしれない。(あっちもアブザンミッドレンジを食える一方、マルドゥにはガン不利。)
とりあえずサイドから対策必須なのは
この中でマルドゥだけはどうやって対策したらいいのかわからん。メインボードに1〜2マナクリーチャーを17枚も採用しているせいで、とにかく神々の憤怒が激烈にキツい。全体のタフネスを3ぐらい上げられる手軽な手段があれば大分違うのだが、今のところそんな都合の良いカードは存在しない。一応、思考囲いでピンポイントで手札を落とせれば大分マシになるのだが。(憤怒さえどうにかできれば勝ち目が出てくる。それでもキツイけど。)
今日はRustとCのバインディングの部分でも。
システムプログラミングを視野に入れているだけあってCとの使いやすいインターフェースはちゃんと用意されている。そして基本的なところは割と簡単で、externブロックにlink属性付けて利用するCのライブラリと関数を列挙して、後はunsafeブロックの中で呼び出すだけ。以下はeditlineを使ったrlcompleterのサンプル。完全なソースはbitbucketより。
extern crate libc;
extern crate shell;
use libc::{free, c_void};
use std::c_str::CString;
use std::ptr;
use shell::ReadLine;
#[link(name = "editline")]
extern {
fn readline(prompt: *const u8) -> *const i8;
fn add_history(h: *const i8) -> c_void;
}
pub struct Rl<'a>;
impl<'a> ReadLine for Rl<'a> {
fn readline(&self, prompt: &str) -> Option<String> {
unsafe {
let raw_str = readline(prompt.as_ptr());
if raw_str == ptr::null() {
return None;
}
add_history(raw_str);
let c_str = CString::new(raw_str, false);
let result = c_str.as_str().map(|s| s.to_string());
free(raw_str as *mut c_void);
return result;
}
}
}
ただこのサンプル、何故かUnknown Commandになった時に入力した文字列がプロンプトに残り続けてしまう。Rustのバグなのか、それとも俺のミスなのか不明だが、なんかRust側がおかしいようにも思える。あとRl::readlineにリテラルの文字列を渡すようにすると、byhelpと常に出力されるという奇妙な挙動を示すようになる。この辺からもRust側が何かやらかしてんじゃないのと思うのだが、調べる時間がない。
というかまあ、Nightly取ってきて弄くってるのでその辺はまあ多少は仕方がない。
追記:Rl::readlineに渡す文字列を一旦cloneでコピーして、そこからCライブラリに渡すようにしたら、上に書いたような不具合が出なくなった。うーむ、バグなのかライフタイム管理に関係した仕様なのかわからん。何となく、バグと仕様両方に起因する問題が同時に起こって切り分けが難しくなっていたように思えるが。
Rustでちょっとパーザーライブラリを作り始めた。既にRustにはパーザージェネレーターが複数あるようだが、俺はパーザージェネレーターが好きではないので、自前でライブラリを作ることにした。(なんでそんなもんが必要かって?言語処理系作るのに必要だからだ。)
まだちょっと遊びと実験でゴチャゴチャやってる段階だけど、ある程度形になったらこれ用にリポジトリを一個作るか。
今のところできる事は大したことがないのだが、いわゆるパーザーコンビネーターっぽい書き方が出きるようには作っている。以下はテストコードからの抜粋。
#![feature(phase)]
extern crate topdown;
#[phase(plugin)]
extern crate topdown_macro;
use topdown::{CharSeq, Parser, ParserResult, S};
use topdown::ParserResult::{Succ, Error, Fail};
#[test]
fn test_bind() {
let mut cs = CharSeq::new("abcdefghi", "<mem>");
let r: ParserResult<int> = bind! (
cs.accept(&S::new("abc")) -> |a|
cs.accept(&S::new("def")) -> |b|
cs.accept(&S::new("ghi")) -> |c| {
assert!(a.as_slice() == "abc");
assert!(b.as_slice() == "def");
assert!(c.as_slice() == "ghi");
return Succ(1i);
});
}
最初は演算子オーバーロードで頑張る予定だったが、演算子のTraitは以下の形になっているのでダメっぽかった。
pub trait Shr<Sized? RHS, Result> for Sized? {
fn shr(&self, rhs: &RHS) -> Result;
}
上記のテストコードではクロージャを渡す格好になっているが、もしも演算子オーバーロードで同じ事をやろうとした場合、以下のような定義になる。
impl Shr<S, T> for ParserResult<S> {
fn shr(&self, f: &|S| -> T) -> ParserResult<T> {
...
}
}
こうなるとクロージャへの参照外しをしなければならないのだが、どうもそれはできないようなので、演算子オーバーロードを使うやり方は頓挫した。まあそもそもあまり行儀の良い使い方ではないといえばそうなので、しょうがないのでマクロでやることにした。ただしRustのマクロはトップレベルの名前空間を使うことになっている上にコンパイル時にインポートする必要があるなど結構癖があるので、マクロだけは別モジュールに移すことにした。
とりあえずはtopdownでやっているのと同程度の事が出きるようになるのが目標。これが出来上がったら、さて次はどうするか。CatyスキーマのRust実装というのはちょっとやってみたい気がするが、しかしアレはアレで実装が結構面倒くさいからなあ。
ひとまずパーザーライブラリだけなら、まあそんなに時間はかからんかな。
またしてもRustの話。
上記はテストコードを書くときに案外使う。いや最初に書いたテストファイルをテンプレート代わりにして次のテストを書き始めたりすると、未使用インポートをいちいち削除するのがタルかったりするんでね。未使用変数も、例えばパターンマッチであるパターンに入ったらそこで束縛した変数の中身見ないで済むケースがあったりするので、そういうところで警告が出るのもなあという。
なのでテストコードの実行の際には-A unsued_variablesと-A unused_importsを付けることにしている。
あとRustの開発環境は(g)vim+racer+rust.vimで作っているが、コード補完が普通にできるのでまあ暫くはこれでいいか。まともなIDEが作られてくれればそれが一番だが。
Rustでライブラリを書くときの基本的なレイアウト。ってかcargoで作るとこうなる。
src/ lib.rs foo.rs bar.rs
単体テストは個々の.rsファイルに書く。lib.rsには共通で使うコードを書く他、複数のファイルをまとめる役割もある。例えばcrateの名前をfoobarにして、foo.rsとbar.rsで定義した物をuse foobar::{...}で使えるようにしたい場合、以下の用にlib.rsに書く。
pub use foo::Foo; pub use bar::Bar; pub mod foo; pub mod bar;
上記の例ではfoo.rsの中身はfooモジュール、bar.rsの中身はbarモジュールにそれぞれ存在しているので、それをトップレベルでpub useする事でフラットな名前空間に置くことができる、と。これがわからなくて結構ハマった。
ここ最近取り組んでいたパーザーライブラリは大体動くようになったのでひとまず公開。まだexamplesは作りかけだが、パーザー部分はちゃんと動いているのでまあいいや。もうちょい弄ったらcargoに登録しよう。
以下は現状の課題。
sturct S1;
impl Parser<SomeType> for S1 {
... //略
}
sturct S2;
impl Parser<SomeType> for S2 {
... //略
}
fn foo() {
let a = S1;
let b = S2;
// let l = [&a, &b]; これはダメ
let l: [&Parser<SomeType>, ..2] = [&a, &b]; // 明示的にtraitを指定
let c = choice(&l);
}
というか大体の問題がchoice回りなので、見直しが必要なのはこのあたりか。
ここ最近書いてたライブラリをCargoに登録した。まだドキュメントを何一つとして書いてないので、まあ使いたい人はサンプルを読みながら気合で使ってくれ。
一応サンプルにはお約束の計算機の他に、簡単なスクリプト言語のインタープリタも付けてある。大体この手のパーザーライブラリを使いたいというのはオリジナルのプログラミング言語を作りたい人だろうから、インタープリタ部分はともかくとして(ここは手抜きしまくったんで)構文解析についてはそれなりに参考になるような、ならないような。
これで一段落したので次に何を作るかだが、時間があればCatyスキーマをRustに移植してみたいとは思う。ただまあ、そっから先のCatyスクリプトの移植とか、インタープリターやWebサーバーの移植がこれまたヘヴィな作業なのでどこまでやるかはわからん。それ以前にCatyスキーマの型計算のコードは書いた俺が二度と見たくない(でも一番バグが出るのはそこなので一番弄る頻度が高い)代物で、あれの移植というのは気力体力ともに充実していないとダメっぽいのでいつから着手するかは不明。