RustでBrainf*ckを実装してみた

  • 17
    いいね
  • 4
    コメント

概要

Rust入門者向けハンズオン以来、Rustを実際に書く機会がなかったので、Advent Calendarの機械に書いてみようという気持ちになり、とてもなんとなくBrainf*ckを実装してみることにしました。
この記事では、実装の途中経過ごとの感想みたいなものをつらつらと書いていきます。

Brainf*ck

Brainf*ckは、難解プログラミング言語の1つで、言語仕様が非常にコンパクトなことが特徴です。
そのため、とりあえず作ってみるにはちょうど良い言語です。

言語仕様

処理系は次の要素から成る: Brainfuckプログラム、インストラクションポインタ(プログラム中のある文字を指す)、少なくとも30000個の要素を持つバイトの配列(各要素はゼロで初期化される)、データポインタ(前述の配列のどれかの要素を指す。最も左の要素を指すよう初期化される)、入力と出力の2つのバイトストリーム。

  • > ポインタをインクリメントする。
  • < ポインタをデクリメントする。
  • + ポインタが指す値をインクリメントする。
  • - ポインタが指す値をデクリメントする。
  • . ポインタが指す値を出力に書き出す。
  • , 入力から1バイト読み込んで、ポインタが指す先に代入する。
  • [ ポインタが指す値が0なら、対応する ] の直後にジャンプする。
  • ] ポインタが指す値が0でないなら、対応する [ (の直後)にジャンプする。

Wikipediaより、Brainf*ckの言語仕様に関する部分のみ抜粋。

初期実装

https://github.com/mihyaeru21/brainfuck-rs/blob/be62aeb27c30074f0f847e59eb2ccd71f182644f/src/main.rs

とりあえずドカドカ実装してみたのがこの状態です。
面倒そうだったので,の実装を横着していました。
この頃は、まだmain.rsmainにベタッと書いた感じです。

ここまで実装した感想

  • if letがSwiftっぽい
    • Swiftと違ってOption<T>専用の機能じゃないのが好きです
    • Swiftみたいに複数の変数にいっきに代入したり、条件も同時に指定できるとなお便利だなーと思って書いていました
  • while letがスッキリして好き
  • 言語において有効な文字が8文字しかないのでmatchで簡単
  • 対応する[, ]を見つける部分がゴリゴリすぎて辛い
  • 参照なのかとかmutなのかとか意識するのが難しい

無駄にstructに切り出してみたり、inputを実装した状態

https://github.com/mihyaeru21/brainfuck-rs/blob/ac9d8ce2ad2336efe98846c641b39b4db7b2b0a7/src/main.rs

やる必要は無さそうな気もするけど処理部分をstructに詰め込んでみました。
それをしつつ、最初に横着していたinputも実装しました。

実は最初の実装では、Brainf*ckのメモリにあたる配列の中身にu32を使っていて正しい実装にはなってませんでした。
この段階でそれに気づきu8の配列に変更しました。
この対応により、マルチバイト文字も(事実上)扱えるようになりました。

テスト時に入出力を標準入出力でない何かに置き換えられるよう、std::io::Stdin, std::io::Stdoutを直接保持せずstd::io::Read, std::io::Write traitを使うようにしてみました。
この時、traitを型とする変数は作れないことを知りました。
Box<T>で包むか、参照の変数にするかすれば良ようです。
たしかに、traitのではコンパイル時にサイズを確定できないので仕方ない感じです。

ここまで実装した感想

  • LLが長かった身としては、変数で持つ時にBox<T>でくるまないといけないのは面倒な感じ
    • Swiftではprotocolの変数を作ることができたので違和感が強かった
  • 入力できるの楽しい

ファイル分割

https://github.com/mihyaeru21/brainfuck-rs/tree/a91e614e9a1705f5eea1e7acc6fcb2a5c8df7c07/src

ちょいちょい実装していったら、structがいくつかできたのでファイル分割を試してみることにしました。

ここまで実装した感想

  • モジュールの仕組みが難しく、頭に入ってくるまでかなり時間がかかってしまった
    • lib.rsとかmod.rsというファイル名に依存する部分が初見殺しっぽかった
    • lib.rs以外では相対パスっぽくなるのもトラップ感が

テスト

https://github.com/mihyaeru21/brainfuck-rs/blob/5516df34a6b2188839236244d706717304c9d336/tests/interpreter_test.rs

機能自体をmainから切り離したのでテストが書きやすくなったので書いてみました。
入出力を実装した時にtraitベースでやっていた部分により、テストコードだけで入出力ができました。

はじめはBox<Read>などで持っていた入出力は、テスト時に外からアクセス出来ないことに気づき、解消方法が結局わからずかなりの時間を消費してしまいました。
Box<Read>ではなく&'a mut ReadにすることでBrainf*ck実行を司るstructと同じ寿命にし、テスト時にはBrainf*ck実行後に所有権がテストコード側に返ってくるような形でお茶を濁しました。
うまいやりかたを知っている方がいたらご教授頂きたいです...

ここまで実装した感想

  • 完全に外部モジュールを使う感覚でテストをすることになるのは振る舞い的には良さそう
  • 所有権の泥沼に...とにかく時間を使ってしまった
  • 入力をそのまま出力するだけの処理だけど、Brainf*ckで🍣を扱えたのが嬉しい

エラー処理をいろいろ追加

https://github.com/mihyaeru21/brainfuck-rs/tree/rust_advent_calendar_2016/src

エラー処理に関する公式ドキュメントを参考に、独自のエラー定義もしてみました。
全体に渡り、例外的な状態が発生しそうな部分でResult::Errを返すようになり、無駄に堅牢な感じに仕上がりました。

ここまで実装した感想

  • 堅牢に書こうとするとやっぱり煩雑になってしまう
  • Errorとして標準のやつと独自のやつをenumで混ぜて?使う感じは良さそう
  • try!()良い感じ
    • 欲を言うとtry! hogeと書きたかった

全体の感想

今回、テストもそれなりに(1個)書いて、堅牢そうな雰囲気のものを作ってみたのは良い経験になりました。
これだけ叱ってくれるコンパイラだけに、コンパイルができたときは常に正常に動かせていた感じでした。
消耗してしまった部分もありますが、こりずにまた何か実装してみます。

リポジトリ

リンクになっているタグがこの記事を書いた時点での成果物です。

https://github.com/mihyaeru21/brainfuck-rs/tree/rust_advent_calendar_2016

この投稿は Rust Advent Calendar 20163日目の記事です。