2014-12

2014-12-04 

ちょっと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の流儀に慣れていないが、いくつかハマったところというか最初ちょっとギョッとした所を。

Stringと&str問題 

これは下記の2つの記事が参考になった。

要するに&strはビューであり、ある処理がその処理対象を所有している必要がない場合はできるだけビューの方を使い、逆に所有している(=オブジェクトのlifetimeを管理している)場合はStringやVec<T>を使った方が良いようだ。

ちょっとコードを書いた限りでは、lifetimeが複雑化したり、あるいはどうしても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であるべきなんじゃないのと思ってこうなった。

とはいえ、この辺はまだ全然自信がない。

Traitのサイズ問題 

Rustはオブジェクトのサイズをコンパイル時に知る必要があるので、サイズが不定なTraitはそのままでは不都合がある模様。なので、Boxで間接参照の形にしてやる必要がある。まあ、これはCで再帰的な構造体を作るときにポインタを使うのと同じような事情だからハマったわけではないか。というか実際、Rustも再帰的な構造体を作るときもBoxで参照の形にしないといけない。


現状の問題は、まだまだ情報が少ないことと、言語の変化が激しすぎること。何しろNighly Build落としてきたら標準ライブラリのレイアウトが変わっていたとかが普通にあるのでなあ。

言語の目指している方向性そのものはかなり支持できるものではある。

2014-12-06 

晴れる屋でプロツアーM16のPPTQがやっていたので行ってきた。競技レベルのイベントは実はこれが初めてで、まあルール適用度の高いイベントに慣れる意味でも。デッキはアブザンの隆盛積んだアブザンアグロ。

Abzan Ascendancy Aggro
2 平地
2 沼
2 森
3 コイロスの洞窟
3 ラノワールの荒原
4 吹きさらしの荒野
1 豊穣の神殿
4 砂草原の城塞
2 マナの合流点
3 万神殿の兵士
4 血に染まりし勇者
4 荒野の後継者
4 羊毛鬣のライオン
2 ラクシャーサの死与え
3 アブザンの鷹匠
4 先頭に立つもの、アナフェンザ
4 包囲サイ
4 アブザンの隆盛
3 アブザンの魔除け
2 不動のアジャニ
1 万神殿の兵士
3 消去
2 思考囲い
1 ラクシャーサの死与え
3 悲哀まみれ
2 不気味な腸卜師
2 真面目な訪問者、ソリン
1 世界を目覚めさせる者、ニッサ

1マナ域7枚、2マナ域10枚、3マナ域7枚、4マナ域4枚と生物を大量に採用し、マナカーブ通りに展開した後に隆盛を張ってゲームセットにする殺る気満々のレシピ。まあ、マルドゥに死ぬほど不利という欠陥があるが、そこはもうマルドゥを踏まないことを祈ってどうにかしようとした。というかアブザンでマルドゥを乗り越えようとしたらアグロ寄りの調整は多分ダメで、ミッドレンジでも除去の山がキツく、アブザンコントロールぐらい思い切って長期戦を見据えないと勝てなさそうだ。

というわけでスイスドロー8回戦の結果。

R1 ステロイド
G1:相手のワンマリに付け込んで勝利。G2前のシャッフルで俺が相手のデッキをめくってしまって警告を喰らったので、今後は要注意という事で。(あんまり警告が累積すると最悪公式戦に出場停止になる。)
G2:文句なしのマナフラッドで死亡。
G3:ワンマリ。相手は本来ビートダウンの構成なのだが、こっちが低マナシフトのアグロデッキだったのでサイドボーディングで対応された上に除去ハンドをキープされ、マルドゥに負けるとき同様に全部捌かれて負け。このデッキは重い黒除去にはある程度耐性があるが、軽くてテンポの取れる火力に滅法弱いんだよな。G2がフラッドだったので相手のプランが全然読めず、それがG3に響いた面もあった。(とはいえ、サイドから入るカードはそんなになかったと思う。囲いで除去以外抜いてデカ物で押し切るプランぐらい?)
R2 青白コントロール
G1:1枚目の対立の終結は完全に読めていたのでケアして生物の展開プランを立てられたが、2枚目を早々と引かれているのは読めなかった。(でもドロースペルをT3に撃っていたし、その後の相手の挙動で気付くべきだったのか?)そこから毅然たる大天使で負け。
G2:非常に微妙な盤面で読み負けて敗北。盤面は相手がニクス毛の雄羊のみ、こっちはアナフェンザと万神殿の兵士とラクシャーサの死与え。全員で殴ってラクシャーサの死与えをパンプするか、手札の包囲サイが通れば勝ち。ただしサイをカウンターされると対立の終結で負けて、相手は青含む2マナを立たせている。かといって何もせずにターンを返した場合、カウンターを構えながら生物を2枚どかされて逆転の目もあった。ただし、相手がその手段を持ってない場合はターン返して対立の終結にラクシャーサの死与え再生で多分勝ち。まったく読みきれなかったので、ブラフかあるいはバウンスかと思ってパンプしたら、ラクシャーサの死与えに突き刺さる貪る光!ニクス毛の雄羊を召集コストに使ってのプレイだった。その後対立の終結の返しに包囲サイを繰り出すも、あとは1対1交換の繰り返しでいいので完全にあちらのペース。どうも正解は何もせずターンを返すだった模様。完全に俺のプレイミスというか、こっちのミスを誘ってそこからしっかり締めた相手が上手かったというべきか。
R3 エスパーウォーカーズ
G1:相手マリガンから初動が遅くて勝ち。対立の終結を2枚撃たれたが関係なかった。
G2:相手が先手T3にアショクという回り方だが、こっちも生物を展開してアショクにあまり仕事をさせず。その後対立の終結で流されたり、アショクの2体目が出てきたり、また対立の終結で流されたり、時を越えた探索を撃たれたりしたが、何かこっちが勝ってた。サイドから入れたソリンがぶっ刺さったのと、やはりアブザンの隆盛でリソース計算が狂うのが厳しいらしい。エルズペスに辿り着かれなかったのも大きい。
R4 スゥルタイランプ
G1:スズメバチの巣で地上が止まり、ガラクの目覚めで流され、キオーラの奥義で終了。
G2:序盤の攻撃と包囲サイで少々削った後、アブザンの鷹匠で全員飛ばして一回の攻撃で15点削って突然死させて勝ち。
G3:相手の土地が4で詰まったのでひたすら殴って勝ち。
R5 白青英雄
G1:荒野の後継者を2体連続で並べて相手の表情が曇る。その後ライオンとラクシャーサを追加、ライオンの怪物化でクロックが跳ね上がって終了。
G2:相手が占術土地1の賭けキープ。あと1マナあれば手札を片っ端からダンプしてブン回りだったが、そこで詰まって勝ち。
R6 アブザンアグロ
G1:何回マリガンしても土地1、死ぬまで土地1(まあ最後2枚目引いたけど手遅れ)。
G2:占術土地があるから土地2キープしたら死ぬまで3枚目を引けずに死亡。土地が5枚固まってるブロックと7枚固まってるブロックがライブラリの底の方にあり、そりゃ引けないわ。
R7 緑単信心
G1:あまりに酷い初手をマリガン、遅めの手札を渋々キープ。先に盤面を固められて女王スズメバチで負け。
G2:大体G1と同じ。緑単信心には序盤からアグレッシヴに動けると勝てなくもないが、初動がちょっと遅れると盤面が固まって一気に持っていかれる。というかマリガンしすぎ。
R8 ジェスカイテンポ
G1:ブン回って勝ち。
G2:相手が割と回った形。終盤で微妙にプレイミスしたっぽいが、結果論から言えばミスってなくても負けていた可能性が濃厚。まあ、もうちょっとどうにかできたか。チャンドラはさっさと始末しないとダメだ。それともうちょっとライフゲイン手段引けてればなあ。
G3:ワンマリから土地2で詰まって死亡。あと1枚引ければ悲哀まみれで逆転してたんだが。

3-5で惨敗。開幕2敗で目なしではあったが、しかしアブザン同型を事故2回で落としてるのが痛すぎた。そこからはもう坂道を転げるように連敗。

そんでまあプレイミスで負けてるのとメタの読み違いとは俺のせいなのでそれは納得だが、しかし落としたゲームを見ると

  • 明確にミスって落としたゲーム:1
  • 多分ミスって落としたゲーム:1
  • 事故で落としたゲーム:4
  • 相手の方が動きが良くて落としたゲーム: 3
  • デッキ相性で負けたゲーム:2

という形。こうして見ると敗因の30%ほどが事故、それもマナフラッドとマナスクリュー。何か最近マジで事故で負けまくってる。色事故とかタップイン多すぎでテンポロスして負けてるだったら、まあ3色アグロというコンセプトに無理があるという事になるのだが、フラッドとスクリューはそれとは別だからなあ。ちょっとこのフラッドとスクリューでの敗北率については何らかの統計データが欲しい気がする。

あと今日の構築ミスとしては、赤単やトークン系を警戒しすぎた結果、中型〜大型クリーチャーをどんどん展開してくる緑単信心やステロイド対策を積めなかったことが挙げられるか。またマルドゥにはどうやっても構造上勝てないので、その点でもキツいかな。今のところ不利な緑単信心はサイドボーディングで戦えるし、緑黒星座も勝てなくもない。しかしマルドゥは本当にゲロ吐くぐらい不利なので、マルドゥがTier 1にいる現状でこれを使い続けるのは茨の道かもしれん。案外、ポジション的には白青英雄と同じかもしれない。(あっちもアブザンミッドレンジを食える一方、マルドゥにはガン不利。)

とりあえずサイドから対策必須なのは

  • 横に並べる赤系・トークン系
  • マルドゥ全般
  • 緑単信心

この中でマルドゥだけはどうやって対策したらいいのかわからん。メインボードに1〜2マナクリーチャーを17枚も採用しているせいで、とにかく神々の憤怒が激烈にキツい。全体のタフネスを3ぐらい上げられる手軽な手段があれば大分違うのだが、今のところそんな都合の良いカードは存在しない。一応、思考囲いでピンポイントで手札を落とせれば大分マシになるのだが。(憤怒さえどうにかできれば勝ち目が出てくる。それでもキツイけど。)

2014-12-08 

今日は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ライブラリに渡すようにしたら、上に書いたような不具合が出なくなった。うーむ、バグなのかライフタイム管理に関係した仕様なのかわからん。何となく、バグと仕様両方に起因する問題が同時に起こって切り分けが難しくなっていたように思えるが。

2014-12-13 

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実装というのはちょっとやってみたい気がするが、しかしアレはアレで実装が結構面倒くさいからなあ。

ひとまずパーザーライブラリだけなら、まあそんなに時間はかからんかな。

2014-12-15 

またしてもRustの話。

  • 未使用変数にはデフォルトで警告が出るが、意図してやってる場合は余計なお世話。その場合、[allow(unused_variables)]という属性を使えば良い。あるいはコンパイラオプションの-Aでも。
  • 未使用インポートの警告はunused_imports。

上記はテストコードを書くときに案外使う。いや最初に書いたテストファイルをテンプレート代わりにして次のテストを書き始めたりすると、未使用インポートをいちいち削除するのがタルかったりするんでね。未使用変数も、例えばパターンマッチであるパターンに入ったらそこで束縛した変数の中身見ないで済むケースがあったりするので、そういうところで警告が出るのもなあという。

なのでテストコードの実行の際には-A unsued_variablesと-A unused_importsを付けることにしている。

あとRustの開発環境は(g)vim+racer+rust.vimで作っているが、コード補完が普通にできるのでまあ暫くはこれでいいか。まともなIDEが作られてくれればそれが一番だが。

2014-12-20 

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する事でフラットな名前空間に置くことができる、と。これがわからなくて結構ハマった。

2014-12-21 

ここ最近取り組んでいたパーザーライブラリは大体動くようになったのでひとまず公開。まだexamplesは作りかけだが、パーザー部分はちゃんと動いているのでまあいいや。もうちょい弄ったらcargoに登録しよう。

以下は現状の課題。

  • とにかくデバッグがやりにくすぎる。run_parser!マクロを使うと顕著。どこまでパーズ出来たのか直観的に分からんので、デバッグ出力みたいなのを挟めるようにすべきかもしれない。一応gdbを使ったデバッグ方法もあるのだが……。
  • lifetime管理の都合上、細かく変数に束縛して行かないとダメなのが面倒。一気に行うためのマクロが必要かもしれんが、それもどうなんだろうなあという。
  • どっちかっつーとRustの型システムの上では仕方のないことかもしれんが、traitを実装したstructのインスタンスの配列の型推論が上手く行かないケースがある模様。
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); 
}
  • 上記のコードの場合、Parser traitの配列ではなくS1の配列と見做されるので、S2のインスタンスへのポインターを入れようとした時点でエラーになるし、またchoiceの引数は&[&Parser<T>]なので、ここで型エラーになる。

というか大体の問題がchoice回りなので、見直しが必要なのはこのあたりか。

2014-12-27 

ここ最近書いてたライブラリをCargoに登録した。まだドキュメントを何一つとして書いてないので、まあ使いたい人はサンプルを読みながら気合で使ってくれ。

一応サンプルにはお約束の計算機の他に、簡単なスクリプト言語のインタープリタも付けてある。大体この手のパーザーライブラリを使いたいというのはオリジナルのプログラミング言語を作りたい人だろうから、インタープリタ部分はともかくとして(ここは手抜きしまくったんで)構文解析についてはそれなりに参考になるような、ならないような。

これで一段落したので次に何を作るかだが、時間があればCatyスキーマをRustに移植してみたいとは思う。ただまあ、そっから先のCatyスクリプトの移植とか、インタープリターやWebサーバーの移植がこれまたヘヴィな作業なのでどこまでやるかはわからん。それ以前にCatyスキーマの型計算のコードは書いた俺が二度と見たくない(でも一番バグが出るのはそこなので一番弄る頻度が高い)代物で、あれの移植というのは気力体力ともに充実していないとダメっぽいのでいつから着手するかは不明。