書くまでの流れ
ふとTiny BASIC
を書きたいと思った
ちょうど達人プログラマーを読んで、ミニ言語を作れ、というTipがあったからかもしれない
Rebuild.fmではそんなのやめとけ!と言われていたような気もするけれど、ちょっとやってみるか!
↓
どうやらパーサコンビネータ(Geal/nomやMarwes/combine)を使うといい感じらしい
↓
全然知識は無いが、いい勉強になりそう
nom
のサンプルを動かしたり、Qiitaの記事(Rustのパーサコンビネータnomを使ってみよう)を真似する
↓
nom
で使う大量のマクロに面食らう
これ、もうミニ言語、もとい、DSLじゃねーか!
↓
上記の記事を書いている人が、Rustのマクロを覚えるも書いていた
なので、マクロに体を慣らそうと考える
↓
Rustのマクロ、薄々気づいてはいたけどめっちゃ強力だな (Lispの影響なのだろうか?)
BASIC
っぽいなにかマクロでいけるんじゃないか???
できたもの
BASICっぽいものができました
以下のものが実行できます
#[allow(non_snake_case)]
#[allow(unused_assignments)]
fn main()
{
interpret_basic!{
PRINT "Hello, world!\n";
LET A = 1 + 1;
LET B = A + 2;
PRINT "A = ", A, ", ";
PRINT "B = ", B;
PRINT ;
LET A = 2;
IF (A == 2) {
PRINT "The condition is matched !\n";
PRINT "A is 2\n";
}
FOR A = 1 TO 10 {
PRINT A, ", ";
}
PRINT;
LET_MUT N = 0;
PRINT "N = ";
INPUT N;
PRINT "Your input number is ", N, "\n";
FN FIZZ_BUZZ(N) {
PRINT "It is ";
IF ((N % 15) == 0) {
PRINT "FizzBuzz\n";
} ELSE {
IF ((N % 3) == 0) {
PRINT "Fizz\n";
} ELSE {
IF ((N % 5) == 0) {
PRINT "Buzz\n";
} ELSE {
PRINT N, "\n";
}
}
}
}
FIZZ_BUZZ(N);
FN FIBONACCI(N) {
IF ((N == 0) || (N == 1)) {
RETURN N;
}
LET_MUT X = 0;
LET_MUT Y = 1;
LET_MUT F = 0;
FOR _ = 2 TO (N + 1) {
F = X + Y;
X = Y;
Y = F;
}
RETURN F;
}
LET F = FIBONACCI(N);
PRINT N, "th fibonacci number is ", F, "\n";
PRINT "FIN\n";
};
}
BASICは変種がたくさんあるようなので、頑張ればBASICって言い張ってもいいかなーと思います
次のものを実装しました
- 変数(整数型のみ)
- 外部入力(整数のみ)
-
IF
,IF ELSE
命令による条件分岐 -
FOR
命令によるループ
main関数の中に、FizzBuzzやフィボナッチ数の計算など、サンプルが雑多に書いてあります
関数はRustのクロージャによる実装のため、再帰呼出しできません。
サンプルのフィボナッチがループで実装されているのはそういう理由です。
実装
Github - mopp/CodeGarage/rust/macro_bs.rs
macro_rules! interpret_basic {
() => {
()
};
(PRINT ; $( $rest:tt )*) => {
println!("");
interpret_basic!($( $rest )*);
};
(PRINT $( $args:expr ),+ ; $( $rest:tt )*) => {
$( print!("{}", $args); )+
interpret_basic!($( $rest )*);
};
(INPUT $var:ident ; $( $rest:tt )*) => {
$var = {
// https://github.com/rust-lang/rust/issues/23818
use std::io::Write;
std::io::stdout().flush().expect("could not flush the stdout.");
let mut number_string = String::new();
std::io::stdin()
.read_line(&mut number_string)
.expect("Failed to read line");
number_string
.trim()
.parse::<usize>()
.expect("The input allows only a number.")
};
interpret_basic!($( $rest )*);
};
(LET_MUT $var:ident = $val:expr ; $( $rest:tt )*) => {
let mut $var: usize = $val;
interpret_basic!($( $rest )*);
};
(LET $var:ident = $val:expr ; $( $rest:tt )*) => {
let $var: usize = $val;
interpret_basic!($( $rest )*);
};
(IF ( $cond:expr ) { $( $inner_if:tt )* } ELSE { $( $inner_else:tt )* } $( $rest:tt )*) => {
if $cond {
interpret_basic!($( $inner_if )*);
} else {
interpret_basic!($( $inner_else )*);
}
interpret_basic!($( $rest )*);
};
(IF ( $cond:expr ) { $( $inner:tt )* } $( $rest:tt )*) => {
if $cond {
interpret_basic!($( $inner )*);
}
interpret_basic!($( $rest )*);
};
(FOR $var:pat = $begin:tt TO $end:tt { $( $inner:tt )* } $( $rest:tt )*) => {
for $var in $begin..$end {
interpret_basic!($( $inner )*);
}
interpret_basic!($( $rest )*);
};
($var:ident = $val:expr ; $( $rest:tt )*) => {
$var = $val;
interpret_basic!($( $rest )*);
};
(FN $name:ident ( $( $args:ident ),* ) { $( $inner:tt )* } $( $rest:tt )*) => {
let $name = |$( $args: usize ),*| {
interpret_basic!($( $inner )*);
};
interpret_basic!($( $rest )*);
};
(RETURN $value:expr; $( $rest:tt )*) => {
return $value;
};
($func_name:ident ($( $args:ident ),*) ; $( $rest:tt )*) => {
$func_name($( $args ),*);
interpret_basic!($( $rest )*);
}
}
解説
コードを貼り付けるだけでは面白くないので、簡単な解説を書いてみます
マクロ自体については上記した、Rustのマクロを覚えると公式ドキュメントを参考に書きました
命令はセミコロン区切り
まず大きな設定として、セミコロン区切り、ということにしました
そのための基本となるコードは以下のようになります
macro_rules! interpret_basic {
() => {
()
};
(PRINT ; $( $rest:tt )*) => {
println!("");
interpret_basic!($( $rest )*);
};
}
一番はじめに実装したPRINT命令を見てみます
matcherが(PRINT ; $( $rest:tt )*)
となっており、
引数無しのPRINT
命令ですね
セミコロンの次にあるのがsingle token tree, tt
の0個以上の繰り返しです
マクロをいまいちわかっていなかったので、繰り返しの解釈に少し手こずりました
この場合であれば0個以上のsingle token treeを受け取る、となります
というか、読みづらい
そして、rest
という名前の通り、PRINT
命令が終わったあとの、全ての残りをここで受け取ります
interpret_basic!($( $rest )*);
のように再帰的にマクロを展開させます
ここで、interpret_basic!
の引数が$( $rest )*
となっていますが、これも繰り返しです
繰り返し展開された$rest
が引数として渡されます
全ての命令を展開し終えたとき、$rest
は空になります
なので、マクロの先頭に
() => {
()
};
という空の展開先を書いています
RETURN
命令以外は全て、このように展開していきます
毎回同じことを書いていて、嫌なので、うまいこと直したかった…
先にセミコロン区切りで、ばらしてから書く命令のマッチングをしようと考えたのですが
一意に展開できない、曖昧だ、と、コンパイラにお叱りを受けてだめでした
PRINT
命令が2通りあるのも同じエラーのためです
INPUT
命令
これはそのままなので、解説しなくてもいいかと思います
stdout
をフラッシュしてやらないと直前に改行が出力されていなかった場合に挙動がおかしくなるので、フラッシュしています
マクロなど関係なしにRustの問題?みたいです
LET
, LET_MUT
命令
これもそのままですね
全部mut
にするとコンパイラがうるさいので、mutableかimmutableで分けました
IF
, IF ELSE
命令
(IF ( $cond:expr ) { $( $inner_if:tt )* } ELSE { $( $inner_else:tt )* } $( $rest:tt )*) => {
if $cond {
interpret_basic!($( $inner_if )*);
} else {
interpret_basic!($( $inner_else )*);
}
interpret_basic!($( $rest )*);
};
(IF ( $cond:expr ) { $( $inner:tt )* } $( $rest:tt )*) => {
if $cond {
interpret_basic!($( $inner )*);
}
interpret_basic!($( $rest )*);
};
書いているときは結構苦労したんですが、見返してみるとなんてことないですね
()
と{}
で囲まれている部分を引っこ抜いて、Rustの普通の構文になおしています
そのあと、それぞれを更に再帰展開します
matcherは上からマッチしていくので、IF ELSE
を先に書くことに注意です
FOR
命令
(FOR $var:pat = $begin:tt TO $end:tt { $( $inner:tt )* } $( $rest:tt )*) => {
for $var in $begin..$end {
interpret_basic!($( $inner )*);
}
interpret_basic!($( $rest )*);
};
FOR
命令もIF
命令と似たようにRustのforに展開して、ブロック内を再帰展開しているだけになります
FN
, RETURN
命令
(FN $name:ident ( $( $args:ident ),* ) { $( $inner:tt )* } $( $rest:tt )*) => {
let $name = |$( $args: usize ),*| {
interpret_basic!($( $inner )*);
};
interpret_basic!($( $rest )*);
};
(RETURN $value:expr; $( $rest:tt )*) => {
return $value;
};
関数の定義と戻り値を返す部分です
最初はRustの普通の関数に対応付けようと思ったのですが、戻り値型の部分をマクロで展開するのがめんどうだなと思い、クロージャを使いました。
(と、書いていて思ったけど、常に何かしらの整数を返すようにすればよかったのでは…)
引数部分をカンマ区切りの0個以上の繰り返しで表現しているので引数の数は好きにできます
RETURN
命令はそのままreturnへの対応付けにしました。
なので、Rustのifの中でもいい感じに動いてくれています。
変数代入/関数呼出し
($var:ident = $val:expr ; $( $rest:tt )*) => {
$var = $val;
interpret_basic!($( $rest )*);
};
($func_name:ident ($( $args:ident ),*) ; $( $rest:tt )*) => {
$func_name($( $args ),*);
interpret_basic!($( $rest )*);
}
マクロで再帰展開していく都合上書いたもので、特に面白みはないですね
感想
BASIC的なのを作ってみたいな欲が多少満たされた
また、90行くらいのマクロでこんなに見た目が変わるんだなと感じた
しかし、便利ではあるが、可読性やとっつきやすさがだいぶ違うのでtraitなどでできることはそっちでやるべきだと思います
振り返ってみると、Rustの構文に対応付けているだけなので、そんなに面白いものでもなかったかな、と感じてしまい、少しだけ残念
だが、マクロがよくわかっていなかったので、とても良い勉強になった