RustのREPL "evcxr" を使ってみた
概要
evcxrはRustのパーサーとコンパイラを外部から呼び出すことでREPLを実現している。セミブラックボックス的なアプローチにも関わらずなかなかの完成度で、今後が期待できる。
evcxr
evcxrは最近公開されたRustのREPL (GitHub, redditのpos)である。また、Jupyterのカーネルもあるため、Jupyter Notebook上でRustを書くこともできる。
Evaluation Context for Rust とのことで、 ev + cx + r と思えば覚えられる。
インストール方法
コマンドラインは以下で入る。
cargo install evcxr_repl
Jupyter版については公式サイトを参照するとよい。
基本的な動作を試してみる。
$ evcxr Welcome to evcxr. For help, type :help >> :help :vars List bound variables and their types :opt Toggle optimization :explain Print explanation of last error :clear Clear all state, keeping compilation cache :dep Add dependency. e.g. :dep regex = "1.0" Mostly for development / debugging purposes: :last_compile_dir Print the directory in which we last compiled :timing Toggle printing of how long evaluations take :last_error_json Print the last compilation error as JSON (for debugging) :internal_debug Toggle various internal debugging code >>
:
がメタコマンド。今のところ終了コマンドはないが、Ctrl+CやCtrl+Dで終了できる。
>> let s = String::from("Hello, "); >> s "Hello, " >> s.push_str("world!"); ^ cannot borrow mutably cannot borrow immutable local variable `s` as mutable
エラーに色がついて、年季のわりに凝っている。
>> let mut s = s; >> s.push_str("world!");
シャドーイングも普通にできる。
1行ずつコンパイルしているため体感がかなり遅い。
>> s "Hello, world!" >> s "Hello, world!"
式はborrowして表示しているっぽい。
>> drop(s); >> s ^ not found in this scope cannot find value `s` in this scope
dropするとなくなる。
>> let s = String::from("ABC"); >> if false { drop(s) }; >> s ^ not found in this scope cannot find value `s` in this scope
実際にはdropされていなくても、同じコンパイルエラーの条件に落ちるときは、なくなった扱いになる。
>> let s = String::from("foo"); >> let z = &s; ^ borrowed value does not live long enough `s` does not live long enough Values assigned to variables in Evcxr cannot contain references (unless they're static) >> { let z = &s; } ()
行をまたいで参照を渡すことはできないが、スコープを切ってやればできる。
>> use std::io::{self, Read, Write, BufRead}; >> use std::fs::File; >> use std::io::BufReader; >> let f = File::open("/proc/cpuinfo").unwrap(); >> let mut f = BufReader::new(f); >> let mut line = String::new(); >> f.read_line(&mut line).unwrap(); >> line "processor\t: 0\n"
インポートは行をまたいで有効。
シリアライズできない値も持ち越せる。
>> extern crate rand;
extern crate
を宣言すると、いい感じに依存関係を追加してくれる。依存関係のコンパイルをするので、時間はかかる。
ここで、手元に evcxr-test
というlibrary crateを作ってみる。内容はこんな感じ。
pub fn fib(n: u32) -> u32 { (0..n).fold((0, 1), |(a, b), _| (b, a + b)).0 }
これは以下のようにして追加できた。evcxrのtemporary directory上にある Cargo.toml
に依存を追加するような仕組みになっているっぽい。
>> :dep evcxr-test = { path = "/home/qnighy/workdir/evcxr-test" } >> evcxr_test::fib(40) 102334155
ただ、 path
はevcxrのtmpdirからの相対パスのため、現状ではほぼ絶対パスしか使えないようだ。
さっき作ったライブラリを書き換えてみる。
pub fn fib(n: u32) -> u32 { (0..n).fold((0, 1), |(a, b), _| (b, a + b)).0 } pub fn foo(n: u32) -> u32 { n }
:dep
でリロードしてみる。
>> :dep evcxr-test = { path = "/home/qnighy/workdir/evcxr-test" } >> evcxr_test::foo(40) 40
リロードできた。
>> struct Foo(u32); Items currently need to be explicitly made pub along with all fields of structs. >> pub struct Foo(u32); Items currently need to be explicitly made pub along with all fields of structs. >> pub struct Foo(pub u32); >> let x = Foo(42); >> let y: Foo = x;
アイテム定義は持ち越されるが、プライベートな定義が含まれてはいけない。(フィールドも含めて)
>> pub struct Foo(pub u32); >> let z: Foo = y; ^ expected struct `user_code_29::Foo`, found struct `user_code_26::Foo` mismatched types
同じ名前をもう一度定義したときは、別のものとして扱われる。シャドーイングしたかのような動作になる。
仕組み
HOW_IT_WORKSという文書があった。Rustで外部REPLを作ろうと思ったらこうなるだろう、という妥当な設計になっている。
- シリアライズできない値も持ち越したい。また、パニックやunsafeによるsegfaultなどもある程度ハンドリングしたい。
- 各行を、あたかも前の行の続きであるかのように実行させるには、インポートとグローバル定義、ローカル定義を引き継ぐ必要がある。
- インポートは単に、過去に実行した
extern crate
とuse
を全部覚えておいて、毎回リプレイすればよい。 - グローバル定義は、過去に定義したものを覚えておいて、前に実行したときの定義を引き継ぐような
use
を生成する。- 行をまたぐと別のモジュールになってしまうため、privateな定義は直感的に振る舞わない。今はそういう定義を丸ごと禁止している。
- ローカル定義の型定義は、変数名と型名を
String
の対として (親プロセスが) 持っておく。 - ローカル定義の実体は、
HashMap<String, Box<dyn Any + 'static>>
という評価文脈として (子プロセスが) 持っておく(おそらくこれが名前の由来)。- そのため、参照は持ち越せない。
- 実際に生成されるコードは、文脈を受け取って文脈を返す関数になる。
- インポートは単に、過去に実行した
rustcにはコンパイル結果をJSONで返す仕組みがあるため、コンパイルエラーに基づく処理は意外と堅牢に動作するらしい。
感想
思ったよりよく出来ている。REPLの用途の一つはライブラリ制作時のちょっとした確認用だが、そのための機能は少々足りていないと感じた。具体的にはtmpdirを使うと、起動するたびに依存ライブラリの再コンパイルが必要なのと、手元のライブラリの参照が面倒なのがネックになる。まさにその問題がTODOに書かれているので今後に期待。
redditの作者コメントによると2ヶ月ちょいでここまでできたとのことなので、今後の発展でかなり良くなる可能性はありそう。