最近 Rust を勉強し始めた初心者です。
trait を使ったポリモーフィックなプログラミングに行くつくまでに大分時間がかかりました。
なんとかコンパイルが通ったところまでメモしておきます。
お題:バージョンごとの評価器
例えばこんな感じのコードを書くにはどうしたらいいか?
// 評価器ファクトリの取得
let factory = &EvaluatorFactory::new();
// バージョン 1.0 の評価器を取得
let evaluator = factory.create("1.0").unwrap(); // ないときは panic
// バージョン 1.0 の評価器に 100.0 を渡して計算した結果を表示
println!("1.0: {} => {}", 100.0, evaluator.evaluate(100.0));
// バージョン 1.2 の評価器を取得
let evaluator = factory.create("1.2").unwrap(); // ないときは panic
// バージョン 1.2 の評価器に 100.0 を渡して計算した結果を表示
println!("1.2: {} => {}", 100.0, evaluator.evaluate(100.0));
ソースコード
// lib.rs
pub mod factory;
// factory.rs
use std::collections::HashMap;
// EvaluatorFactory はバージョンごとの評価器のインスタンスを保持する
pub struct EvaluatorFactory<'a> {
tab: HashMap<&'a str, Box<dyn Evaluator>>,
}
impl<'a> EvaluatorFactory<'a> {
// new はコンストラクタ
pub fn new () -> Self {
let mut f = EvaluatorFactory {
tab: HashMap::new()
};
f.setup_evaluators();
f
}
// versions は保持するバージョンのリストを返す
pub fn versions(&self) -> Vec<&str> {
let mut r:Vec<&str> = vec![];
for key in self.tab.keys() {
r.push(key);
}
r
}
// is_avalable_version は有効なバージョンかチェックする
pub fn is_avalable_version(&self, version:&str) -> bool {
self.tab.contains_key(version)
}
// create は指定されたバージョンの評価器を返す
pub fn create(&self, version:&str) -> Option<&Box<dyn Evaluator>> {
self.tab.get(version)
}
// setup はバージョンごとの評価器を登録する
fn setup_evaluators(&mut self) {
// 各バージョンごとの評価器を登録する
self.tab.insert("1.0", Box::new(_Linear{a:1.0, b:0.0})); // f(x) = x
self.tab.insert("1.1", Box::new(_Linear{a:1.0, b:1.0})); // f(x) = x + 1
self.tab.insert("1.2", Box::new(_Linear{a:2.0, b:1.0})); // f(x) = 2x + 1
self.tab.insert("2.0", Box::new(_Quadric{a:1.0, b:0.0, c:0.0})); // f(x) = x²
self.tab.insert("2.1", Box::new(_Quadric{a:1.0, b:2.0, c:1.0})); // f(x) = x² + 2x + 1
self.tab.insert("3.0", Box::new(_Cubic{a:1.0, b:3.0, c:3.0, d:1.0})); // f(x) = x³ + 3x² + 3x + 1
self.tab.insert("3.1", Box::new(_Cubic{a:1.0, b:-2.0, c:1.0, d:0.0})); // f(x) = x³ - 2x² + x
// ★今後この位置に新しい評価器を追加するコードを挿入する★
}
}
// 評価器のトレイト
pub trait Evaluator {
// evaluate は与えられた値で計算する
fn evaluate(&self, x:f32) -> f32;
}
// _Linear は一次関数で処理する評価器のテンプレ
struct _Linear {
a: f32,
b: f32,
}
impl Evaluator for _Linear {
fn evaluate(&self, x:f32) -> f32 {
// ax+b
self.a * x + self.b
}
}
// _Quadricは二次関数で処理する評価器のテンプレ
struct _Quadric {
a: f32,
b: f32,
c: f32,
}
impl Evaluator for _Quadric {
fn evaluate(&self, x:f32) -> f32 {
// ax²+bx+c
self.a * x * x + self.b * x + self.c
}
}
// _Cubicは三次関数で処理する評価器のテンプレ
struct _Cubic {
a: f32,
b: f32,
c: f32,
d: f32,
}
impl Evaluator for _Cubic {
fn evaluate(&self, x:f32) -> f32 {
// ax³+bx²+cx+d
self.a * x * x * x + self.b * x * x + self.c * x + self.d
}
}
// ★今後この位置に新しい評価器の定義を挿入する★
// main.rs
use study13::factory::EvaluatorFactory;
fn main() {
// 評価器ファクトリの取得
let factory = &EvaluatorFactory::new();
// バージョン 1.0 の評価器を取得
let evaluator = factory.create("1.0").unwrap(); // ないときは panic
// バージョン 1.0 の評価器に 100.0 を渡して計算した結果を表示
println!("1.0: {} => {}", 100.0, evaluator.evaluate(100.0));
// バージョン 1.2 の評価器を取得
let evaluator = factory.create("1.2").unwrap(); // ないときは panic
// バージョン 1.2 の評価器に 100.0 を渡して計算した結果を表示
println!("1.2: {} => {}", 100.0, evaluator.evaluate(100.0));
// 対応するバージョンを表示
println!("versions: [{}]", factory.versions().join("]["));
// 色々なバージョンの評価器で計算させる
process(factory, "1.0", 100.0);
process(factory, "1.1", 100.0);
// バージョン 1.2 の評価器が有効なら計算する
if factory.is_avalable_version("1.2") {
let evaluator = factory.create("1.2").unwrap();
println!("1.2: {} => {}", 100.0, evaluator.evaluate(100.0))
}
process(factory, "2.1", 100.0);
process(factory, "3.1", 100.0);
process(factory, "3.2", 100.0);
}
// process は指定されたバージョンの評価器をファクトリから取得し、与えられた値で計算させる
fn process(factory:&EvaluatorFactory, version:&str, value:f32) {
match factory.create(version) {
// 指定のバージョンの評価器が得られたとき
Some(e) => println!("{version}: {value} => {result}",
value=value,
version=version,
result=e.evaluate(value)), // 評価器で計算した結果
// 未知のバージョンのときはエラー
None => println!("[ERROR] Unknown version: {version}",
version=version)
}
}
実行結果
C:\work\study13>target\debug\study13.exe
1.0: 100 => 100
1.2: 100 => 201
versions: [3.1][1.0][1.2][1.1][2.0][3.0][2.1]
1.0: 100 => 100
1.1: 100 => 101
1.2: 100 => 201
2.1: 100 => 10201
3.1: 100 => 980100
[ERROR] Unknown version: 3.2
ここまで Rust を使っての感想。
- Trait を実装したインスタンスを扱うのに "dyn" という修飾子が必要というところまではコンパイラのエラーメッセージで知りました。
- その先はかなり試行錯誤をし、ググりまくって、"Rust By Example" の 16.2 にたどり着いて Box の存在を知りました。ヒープデータを使うことをコンパイラに教えてやらなければならないとは。
- それから HashMap を使ったらコンパイラのエラーメッセージで「ライフタイム」というものを知りました。この機構も Rust の神髄の一つなのでしょうが、今一つ理解が追い付いていません。もっと Rust でコーディングしまくって身につけていくしかなさそうです。
- Option 型は慣れるといい感じ。「データが存在しない」ケースを意識すると、「データの有無をチェックする段階」と「データを取得する段階」の二つにわかれるのが一般的。でも Rust では返り値の型に Option 型を使うことでスムーズに一発で済ませるられるようになってるのがいいですね。
- println! のプレースホルダ "{}" を多数使うときは "{label}" のようにしてプレースホルダにラベルをつけると順番などの間違いが少なくなってよさそう。今回の main.rs の process 関数の中でこれ見よがしに使ってみました。
- factory.rs の評価器を登録する setup_evaluators メソッドの中身は設定ファイルにしたり、あるいは JavaScript などのスクリプト言語で評価器を定義できると便利そう。Rust で JSON は扱えるのか?Rust で他のスクリプト言語を処理できるライブラリはあるのか?