scheme in rust
scheme in rust
前回はrustのメモリー管理って亊で調べていたら、lispぽいやつが解説されてた。 ならば、それに特化したやつを探してみるかな。
(How to Write a (Lisp) Interpreter (in Python)) を、色々な言語で実装してみるってのが流行してる。で、rustでもやってみたいよねと言う人が、当然居るはずである。下記は、その二人に登場してもらったのを、しげしげと眺めている図であります。
目的はlispを実装する亊ではなくて、実装の考えかたをrust流にどう解釈されてるかを、理解する亊です。色々な人の理解をコードを通じて得ようという訳。
その弌
risp > (+ 1.2 3.4) // 🔥 => 4.6
最初はグーじゃないけど、関数は、add,subしか無い簡易版。数値は浮動少数タイプもOK。いかん、いかん、最初の目的を忘れているね。
#[derive(Clone)] enum RispExp { Symbol(String), Number(f64), List(Vec<RispExp>), Func(fn(&[RispExp]) -> Result<RispExp, RispErr>), }
大事なのは、この列挙型だ。どんなLispの型を扱えるか宣言してる。そしてその型がrustでどう表現されるか、見てとれる。と、偉そうにしてるけど、前回の最後に出てた
enum List { Cons(i32, List), Nil, }
これの意味が、解らないかった。rustがListってモジュールを提供してて、その中でConsとかNilてやつをサポートしてるって、盛大な誤解をしてたのさ。で、一生懸命にそんなモジュールを探してしまったぞ。
で、どうもおかしい。Listなんてそれっぽい名前は止めて、Hogeでいいだろうとか、Consも止めてPairでもいいかな? なんてやってみたのさ。バッチリOKだった。で、得た結論は、自前で勝手に定義していいんだって亊。まあ、 The Rust Programming Language 日本語版 とかを読めば、ちゃんと説明が有るんだけどね。説明の為の説明なんで、頭になかなか入らないのよ、と、弁解。
やっぱり、現場で疑問を持って、原典に当るのが、オイラーの性に合っているな。少々回り道っぽいけどね。
とか、思っていたら、タイミング良く Rustの構造体と列挙型を理解する が、紹介されてた。[解決!Python]クラスを定義するには なんかより、スッキリしてるな(贔屓してません?)。
#[derive(Clone)] struct RispEnv { data: HashMap<String, RispExp>, }
構造体が大事ってのは、経験から知っていたよ。新しい木箱に出会ったら、まず見ておくべき部分だ。昔の本に、まずはデータ構造を見ておけってのがあったけど、それにあたるね。
use std::io::{stdout, Write}; fn main() { let env = &mut default_env(); loop { print!("risp> "); stdout().flush().unwrap(); let expr = slurp_expr(); match parse_eval(expr, env) { Ok(res) => println!("// 🔥 => {}", res), Err(e) => match e { RispErr::Reason(msg) => println!("// 🙀 => {}", msg), } } } }
プロンプトで改行しちゃうんで、しないようにflushを使ってみた。それから、閉括弧はLisp風にしてみたけど、似合わないか。
risp> (+ 3 5) // 🔥 => 8 risp> (* 3 4) // 🙀 => unexpected symbol k='*'
その弐
もう一人の方の解説
Building a Scheme Interpreter in Rust: Part 1
パートが進むにつれて複雑になっていく。けど、上で挙げた目的からすれば、
seanchen1991 / scheme-repl
のパート1を見るだけで十分と思える。お好みで、 post_1
の所を切り換えれば、りっぱなschemeが出現するだろう。
この作者の方、名前から中国系っぽい、rustのコントリビューターらしいので、内容は確かだろう。そんな上から目線でいいのか? 我以外皆我師をモットーとするオイラーに、あるまじき発言だな。
scheme::parse_eval::h05b73bbca79fdc1c (input=..., env=0xcf7ed9d0) at src/main.rs:167 167 let evaluated = eval(&parsed, env)?; (gdb) n 169 Ok(evaluated) (gdb) p parsed $3 = scheme::Expression (gdb) p &parsed $4 = (scheme::Expression *) 0xcf7ed848 (gdb) p *env $6 = scheme::Env { operations: HashMap(size=2) = { ["-"] = scheme::Expression, ["+"] = scheme::Expression } }
ガチガチに固められていて、中身を確認出来無いなあ。何か旨い手は無いものか? OpenBSDのqgdbが使いにくいなら、リナ系は? 帯に短し襷に長し。いらん情報は出てくるものの、ユーザーが欲しい情報は出ず。
こうなったら、昔ながらのprintでdebugだな。
fn eval(exp: &Expression, env: &mut Env) -> SResult<Expression> { println!("==> {}", exp); match exp { :
evalの入口で引数をprint。幸いな亊に、schemeで言うprint相当がインプリメントされてたんで、利用させて貰った。
evalの出口も表示させたかったけど、組み込みが面倒なので諦めた。ちゃんと出来てれば、schemeで言うtraceが実現出来たんだけどね。
scheme > (+ (+ 3 4) (- 10 2)) ==> (+,(+,3,4),(-,10,2)) ==> + ==> (+,3,4) ==> + ==> 3 ==> 4 ==> (-,10,2) ==> - ==> 10 ==> 2 > 15
scmでtraceを味わっておく
> (trace +) #<unspecified> > (trace -) #<unspecified> > (+ (+ 3 4) (- 10 2)) call + 3 4 retn + 7 call - 10 2 retn - 8 call + 7 8 retn + 15 15
入門辺
そして、いよいよPeter Norvigさんのくびきから逃れて、自由な発想で、schemeを実装してる人達に出会います。探したのは、githubで、下記の検索をして、チョイスしました。
Implementation of Scheme interpreter in Rust
README.mdにあるように、ご本尊のconsが登場してます。そのあたりがどうなってるか、みたい。で、ソースのファイル名をみてたら、test.rsが有ったので。
sakae@deb:/tmp/Zefick-scheme/src$ cargo test running 4 tests test object::tests::test_format ... ok test parser::tests::lexer_test ... ok test parser::tests::parser_test ... ok test tests::eval_test ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
真面目にtestを実施してるなら、良い仕様書だと思うぞ。
次は定石に則り、enum を点検。pubを付けてあるのが重要。
grep --color -nH --null -e enum * functions.rs:9:pub enum Function { object.rs:6:pub enum Number { object.rs:12:pub enum Object { parser.rs:4:enum Token { service.rs:55:/// Since Rust doesn't support types for enum variants,
#[derive(PartialEq, Debug, Copy, Clone)] pub enum Number { Float(f64), Integer(i32), } #[derive(PartialEq)] pub enum Object { 4 implementations Nil, Boolean(bool), Symbol(String), String(String), Number(Number), Pair(Rc<Object>, Rc<Object>), Function(Function), }
Pair型が出て来たな。consの枠組みになるんだな。Numberの中身が別の所で定義されてるenumって、名前がかぶっていて少々分かりづらい。本物のschemeは、有理数とか、複素数とか色々なるうちの限定採用なんで、それっぽい名前にして欲かったね。
list.rs
pub fn cons(obj: Rc<Object>) -> Result<Rc<Object>, String> { let (car, cdr) = expect_2_args(&obj, "cons")?; Ok(Rc::new(Object::Pair(car, cdr))) }
もう、すっきりした関数だな。引数が2つある亊確認しつつ取出し。それをペアにしてるだけ。 巷のconsは、新しいセルをフリーリストから切り取ってきて、それにセットしてるけど、そんな面倒は無し。
/// The family of functions which names match with pattern `c[ad]{1,4}r`. pub fn cadr(name: &str, obj: &Rc<Object>) -> Result<Rc<Object>, String> { let mut obj = expect_1_arg(&obj, name)?; for op in name[1..name.len() - 1].chars().rev() { let pair = check_pair(&obj)?; obj = Rc::clone(if op == 'a' { pair.0 } else { pair.1 }); } return Ok(obj); }
car,cdrが、scope.rsに登録されていなくて、どうなってるのと思ったら、まとめて処理してた。pair.0とかは、タプルのエレメントを指定する方法ね。 無限cxr を、shiroさんが、書いておられたな。対抗してるかと思ったら
(caaaaaaaaaaaaaadr 1) Error: unbound variable 'caaaaaaaaaaaaaadr'
こんな風に怒られた。いい所まで来てるのに、もったいないな。制限をどこでかけてるの?
pub fn eval(obj: &Rc<Object>, scope: &Rc<Scope>) -> Result<Rc<Object>, String> { match obj.as_ref() { // resolve a symbol Object::Symbol(s) => { if (s.starts_with("c") && s.ends_with("r")) && (s.len() >= 3 && s.len() <= 6) && (s[1..s.len() - 1].replace("a", "").replace("d", "")).is_empty() { Ok(Rc::new(Object::Function(Function::Dynamic(s.clone())))) } else { (scope.get(s)).ok_or_else(|| format!("unbound variable '{}'", s).to_string()) } }
冒頭部分で、チャックしてた。長い cxr のサイズチェック、続いて、a か d が続いているかチェック。削除してって、残ってたら、それは、a,d 以外の文字が混入してたって判断。変に正規表現を持ち出さない所が偉いな。
ちょいと意地悪で、ドンドコとエリアを食い潰す奴を定義。consの中身は何でもいいんだけど、どこまで行っても終らないpiもどきを使ってみる。関数名は、永久保証の保険屋さんの名前から拝借。
(define (ever l) (ever (cons 3.1415926 l))) () (ever ()) thread 'main' has overflowed its stack fatal runtime error: stack overflow Abort trap (core dumped)
consエリアが尽きるより先に、スタックが無くなったしまったのかな。それならば、rustだけで、もどきを作ってみる。
enum List { Cons(f64, Rc<List>), Nil, } use std::rc::Rc; use List::{Cons, Nil}; fn main() { let mut xx: Vec<List> = Vec::new(); let mut cnt = 0; loop { xx.push(Cons(3.1415926, Rc::new(Nil))); cnt += 1; if cnt % 1_000_000 == 0 { println!("{}", cnt / 1_000_000); } } }
schemeの意図した所とは微妙に違うけど、ドンドコとConsエリア(とVec)を消費するコードを書いてみた。1M毎に、進行状態を報告。
59 Killed
普通に実行すると、いきなり殺された。これだからリナって奴は(以下、自粛)。
57 58 Program terminated with signal SIGKILL, Killed. The program no longer exists. (gdb)
gdb上からだと、少しは防波堤の機能が働いている。それにしても、即死させられるの怖い。まて、OpenBSDの挙動を調べてみるか。
31 32 memory allocation of 32 bytes failed /tmp: write failed, file system is full Abort trap
ちゃんとメモリー不足って言ってきたね。どこかのリナとは違うんです!! それから、 write failedになってるのは、/tmpで実験してるんだけど、そこの容量不足の為、coreを収納出来無かった為だ。
topで挙動を確認してると、実メモリー不足になって、swapが動員され始め、そのうちに、どうにもメモリーのやりくりが出来無くなり、根をあげるって挙動がよく観察できた。
(gdb) bt #0 scheme::lists::cadr::hefc2de276aa1c499 (name="car", obj=0xcf7d9844) at src/lists.rs:8 #1 0x15e87611 in scheme::functions::Function::call::h9c16ada508c9c978 ( self=0x51018fcc, args=Rc(strong=1, weak=0) = {...}) at src/functions.rs:23 #2 0x15e81a0b in scheme::eval::eval::h8e563bf663ac05e6 (obj=0xcf7d9b24, scope=0xcf7da114) at src/eval.rs:148 #3 0x15e973d4 in scheme::main::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::h7dca2da5a8c13acf (obj=...) at src/main.rs:58 :
一応、お約束でバックトレース。モジュールに分割された作りになってる。
リファクタリング
上でちょいと文句を垂れた、Number(Number)て言う、紛らわしいやつ。リファクタリングと称して、Number(PSNumber)に変更してみた。新につけた接頭語PSは、パーシャル・スキームの積もり。
その結果、わんさかと7個もエラーが出現。例えば
error[E0432]: unresolved import `crate::object::Number` --> src/math.rs:1:20 | 1 | use crate::object::Number::{Float, Integer}; | ^^^^^^ could not find `Number` in `object`
こんなエラーだ。今迄、全く気にしてなかったけど、これ、オブジェクト一族の数字群って亊なのね。こういう風に、実践で勉強になるな。いじり回してみろってのは、この亊か。
最初は7個のエラーと思ってたけど、修整を始めると、エラーが増えた。上位でエラーが出ると、下部のチャックはしないと言うコンパイラーの方針なんだな。甘くみてると痛い目にあうぞ。 まあ、fly-checkが走っているおかげで、修整すべき部分は赤く表示されるから、機械的な作業ではあるけどね。
少し高度な奴
もう少し違ったものをって亊で、 scheme.rs を見付けてきた。
buildすると、結構警告が出て来る。
warning: `scheme-rs` (bin "scheme-rs") generated 10 warnings (7 duplicates) Finished dev [unoptimized + debuginfo] target(s) in 19.32s
10個あるって亊は、もっと厳格に査察したら、隠れヤバイヨ・ヤバイヨが炙り出されるに違いない。厳格版は、
ob$ cargo clippy : error: written amount is not handled. Use `Write::write_all` instead --> src/repl.rs:13:9 | 13 | io::stdout().write(b"scheme.rs> ").unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[deny(clippy::unused_io_amount)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy /master/index.html#unused_io_amount warning: `scheme-rs` (lib) generated 84 warnings error: could not compile `scheme-rs` due to 15 previous errors; 84 warnings emitted
こんな風に、clippyってオプションを使う。普通版だと、ちゃんとビルド出来ていたんだけど、コンパイルが失敗に終っている。全く融通の効かない査察官だ。
そういえば、ジェネリック医薬品の不都合な真実 なんて本を読み始めたのよ。アメリカで、口に入れる物全て(食料、飲料、医薬品)を監督する官庁であるFDAとジェネリック薬品メーカーの攻防をルポした本だ。
この本に出て来る査察官みたいだな。相手はインドの薬品メーカー、読み始めたばかりなんで、どう展開してくかは、今後の楽しみ。
先発メーカーのものと同等品ってのの重要なファクターとして、血液中に溶け出す有効成分の濃度が、既定内に入っている亊ってのがあるそうな。不純物の合有とかはどう既定してるのだろう? ジェネリック品を使う身としては、非常に興味があります。
まあ、先発メーカーは価格を自由に付けられる。ファイザーだとかモデルナとか、膨大な利益をあげているはず。ジェネリックは、まだか。そんなに言うなら、寿司のジェネリック品、回転寿司にでも行っとけ。
ああ、脱線しちゃったな。ちょいと査察してみる。
scheme.rs> (define (ever l) (ever (cons 3.1415926 l))) scheme.rs> (ever '())
さっぱり尻尾を出しませんよ。どうなってんの? じっくり査察だな。 それより、他の奴も視野に入れてみるか。複数メーカーを選んでおくってのは、危険防止になりますからね。
本格派の奴
として、 vonuvoli-scheme なのを見付けた。
いさんでコンパイルしてみると
sakae@pen:/tmp/rust-scheme/vonuvoli-scheme$ cargo b : error[E0554]: `#![feature]` may not be used on the stable release channel --> /home/sakae/.cargo/registry/src/github.com-1ecc6299db9ec823/blake2-rfc-0.2.18/src/lib.rs:19:31 | 19 | #![cfg_attr(feature = "simd", feature(platform_intrinsics, repr_simd))] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ : For more information about this error, try `rustc --explain E0554`. error: could not compile `blake2-rfc` due to 3 previous errors
あえなくエラー発生。手掛かりを表示させると、
sakae@pen:/tmp/rust-scheme/vonuvoli-scheme$ rustc --explain E0554 Feature attributes are only allowed on the nightly release channel. Stable or beta compilers will not comply. Erroneous code example: ``` #![feature(lang_items)] // error: `#![feature]` may not be used on the // stable release channel ``` If you need the feature, make sure to use a nightly release of the compiler (but be warned that the feature may be removed or altered in the future).
夜なべ仕事で特徴を追加してるんで、最先端のrustを使え。だけど、それって気紛れかも知れないので保証の限りではない、とな。
果たして、こういう危い木箱を使う必要って有るの? はなはだ疑問。
sakae@pen:/tmp/rust-scheme/vonuvoli-scheme$ ls vendor/ | wc 100 100 1019
使っている木箱を、cargo vendor でまとめてみて、その個数を数えてみた。木箱が増えるほど、こういう地獄を見る機会が多くなる。前にもあったよな、psutilで、木箱がちゃんとコンパイル出来無い事例が。
その対策として、Cargo.lockが用意されてて、木箱のバージョンをロックしちゃうってのがあった。けど、今回のように環境の差異とか、後追いでやろうとしたら、リポジトリィーが変ってしまってて駄目だったってのには対抗出来無い。OSSだから根本解決は無理。
build vs. check
上の長大のしかも失敗するやつに、懲りた。初めてのやつは素早く合否を判定したいぞ。 そこで、buildした場合とcheckした場合で、どれぐらいの差があるか、確認してみる。
ob$ time cargo b : Finished dev [unoptimized + debuginfo] target(s) in 28.90s 0m28.93s real 0m39.79s user 0m43.58s system
ob$ time cargo c : Finished dev [unoptimized + debuginfo] target(s) in 26.93s 0m26.95s real 0m30.34s user 0m39.23s system
4CPUのOpenBSDなんだけど、何も指定しなくても複数のCPUが動いているみたいだな。
カタログ
皆さん、木箱を色々活用してるけど、どうやって星の数程有る中から見つけ出しているの? https://crates.io/ が、一択?
まあ、それでもいいんだけど、別のカタログもみたいぞ。ハードをやってた時代は、石のカタログ(トランジスタ)と言えば、CQ出版のカタログのお世話になったし、ICと言えば、テキサス・インスツルメントのSNXXシリーズの規格表のおせわになったものだ。
その乗りで、部品を探したい。そんなんで見付けてしまったのが、 lib catlog だ。カテゴリー分けがしてあるので、割と探し易い。これの嬉しいのが、依存関係も表示してくれる亊。依存が少ければ少ない程安全だ。それから、作者が途中で放り出していないかも、そこはかとなく分る。
もう一点、 Awesome Rust 素敵なRustってのも素敵だ。便利に使いたい。