概要
C++やPythonでシグネチャが違う関数でも
関数の参照と引数リストの組み合わせのテーブルで呼び出すことができRustでもできないかなと、ふと思ったのでRustでできるか検証してみた。
注意
この記事を書いている人(本人)は、Rustを全然勉強しておらずRustのバージョンすら追っておりません。また、大してRustを現場で使ったりもしていないので適当言っている可能性があります。
理想
理想コードはこんな感じのがRustでできないかを検証。
func1(v: u32) ->() {...}
func2(name: &'static str, v: u32) ->() {...)
args1 = (1,);
args2 = ("jun", 3);
actions = [ # 検証2
(func1, args1),
(func2, args2),
]
for ... { # 検証4:
f, args = actions[..]; # 検証3:
f(args); # 検証1:
}
検証1:f(args)ができるか
Rust では、args がタプルでも 関数呼び出し時に自動で引数展開されない為 f(args) は「1個の引数(タプル)を渡す」意味になり、f(u32) のような関数には渡せないので使いずらい。
ちなみに、なぜargsをTupleにしているかというと、検証3のコードでf, argsにしてどんな引数が来ても取り扱いやすくする為。
tupleops:
参考
このクレートでは、applyという関数で関数とTupleの関数呼び出しが可能になるらしいので検証。
use tupleops::apply;
fn f1(value: u32) -> () {
println!("{}", value);
}
fn f2(name: &'static str, value: u32) -> () {
println!("{} {}", name, value);
}
fn main() {
apply(&f1, (1,));
apply(&f2, ("jun", 2));
}
出力は以下の通りで。想定通りの動きをしてくれるのでこのクレートを最終的に呼び出す。
1
jun 2
まとめ:
Rustはタプルの自動引数展開がないので f(args) はできない。
必要なら tupleops::apply などで明示展開する方法がある。
検証2:actions = [(func1,args1),(func2,args2)] が作れるか?
型定義をせずに、異なるシグネチャの関数や、引数リストを一括で管理できる方法があるかを検証。
frunk::HList:
RustのfrunkクレートでHListというのがあり、複数の型を一つの配列として格納しているように見えて、HeadとTailがネストした「型レベルの連結リスト」という偉大なクレートが存在してくれています。
注意:コンパイル時に型が完全に決まっているものだけ入れれます。
実際に検証1の関数や引数を一つにまとめることができます。
use frunk::{self, hlist};
...
fn main() {
...
let tests = hlist![
(f1, (1,)),
(f2, ("jun", 2))
];
まとめ:
Vec/配列は同型しか入らない。
異種を並べるなら HList のように型を全部保持するリストがあり使用すると事で課題を解消することができる。
検証3:f, args = actions[i] のように取り出せるか?
配置はできたが、どのように扱うのかよく分からないのでHConsドキュメントを調査すと、getというメソッドがありそうなので、hlistの中身を取得できるかを検証。
use frunk::indices::Here;
use frunk::indices::There;
let tests = hlist![
(f1, (1,)),
(f2, ("jun", 2))
];
// let a = tests[0]; 配列では無いので、この記述はダメだった
let _a = tests.get::<_, Here>(); // 先頭の要素を取得
// 次の要素とUnpack
let &(f, args) = tests.get::<_, There<Here>>();
println!("{:?}", std::any::type_name_of_val(&f));
println!("{:?}", args);
HConsのgetメソッドは「どの型の要素」か「どの位置から」で取得できるみたいです。
今回検証したのは「どの位置から」を検証しHereとThereを使う事で要素を取得しUnpackも可能というのが分かったが、
課題として、リストの要素が多い場合、There<There<...<Here>>..>を書くのがしんどい。
検証3の課題:There<There<...>>を代替えできるか検証
There<There<Here>>はどうしても必要なので、エリアス化して呼びやすい形にできるかを検証することにした。
Rustではtype aliasname = で型を別名にできるので、以下の様にコード書き換え実行できることを確認。
type I0 = Here;
type I1 = There<Here>;
...
let _a = tests.get::<_, I0>(); // 先頭の要素を取得
// 次の要素とUnpack
let &(f, args) = tests.get::<_, I1>();
println!("{:?}", std::any::type_name_of_val(&f));
println!("{:?}", args);
上記のようにtypeを定義すれば動作することが分かったが全部定義するのがダルイので
次の様にマクロでカバーすることにした。
seq_macro:
seq_macroというクレートを使えば、同じパターンのコードをまとめて自動生成するためのマクロを用意してくれているみたいなので以下の様にした。
// Here, Thereをラップしてくれるマクロ
macro_rules! define_frunk_indices {
($vis:vis, $i0:ident $(, $rest:ident)* $(,)?) => {
$vis type $i0 = ::frunk::indices::Here;
define_frunk_indices!(@next $vis, $i0 $(, $rest)*);
};
(@next $vis:vis, $prev:ident $(,)?) => {};
(@next $vis:vis, $prev:ident, $i:ident $(, $rest:ident)* $(,)?) => {
$vis type $i = ::frunk::indices::There<$prev>;
define_frunk_indices!(@next $vis, $i $(, $rest)*);
};
}
// 最大値を変更するだけでi0 ~ INまで行けるようになった。
seq!(N in 0..=9 {
define_frunk_indices!(pub, #(
I~N,
)*);
});
これによりI0 ~ I9までが定義される。
まとめ:
HListの取り出しは get でできるが、戻り値は &T。
さらに位置指定は Here/There<...> のような型レベルindexを指定しないといけない。
検証4: for ...をHListに対して行えるか検証
HListは実行時のindexでアクセスできない。
代わりに seq! で コンパイル時に index を列挙して展開することで、疑似的に 回しているように書けるようにする
let tests = hlist![
(f1, (1,)),
(f2, ("jun", 2))
];
seq!(N in 0..=1 {
let &(f, args) = tests.get::<_, I~N>();
println!("{:?}", std::any::type_name_of_val(&f));
println!("{:?}", args);
});
まとめ
実行時ループは不可。seq! で コンパイル時に index を列挙して疑似ループ化する
検証まとめ
各検証結果を元に最終的なコードが以下の様になりました。
use tupleops::apply;
use frunk::{self, hlist};
use seq_macro::seq;
macro_rules! define_frunk_indices {
($vis:vis, $i0:ident $(, $rest:ident)* $(,)?) => {
$vis type $i0 = ::frunk::indices::Here;
define_frunk_indices!(@next $vis, $i0 $(, $rest)*);
};
(@next $vis:vis, $prev:ident $(,)?) => {};
(@next $vis:vis, $prev:ident, $i:ident $(, $rest:ident)* $(,)?) => {
$vis type $i = ::frunk::indices::There<$prev>;
define_frunk_indices!(@next $vis, $i $(, $rest)*);
};
}
// I0 ~ I9までのエリアスを定義
seq!(N in 0..=9 {
define_frunk_indices!(pub, #(
I~N,
)*);
});
fn f1(value: u32) -> () {
println!("{}", value);
}
fn f2(name: &'static str, value: u32) -> () {
println!("{} {}", name, value);
}
fn main() {
let tests = hlist![
(f1, (1,)),
(f2, ("jun", 2))
];
seq!(N in 0..=1 {
let &(f, args) = tests.get::<_, I~N>();
apply(&f, args);
});
}
かなり、自分が理想とするコードの再現がRustでも行えたので、今回は満足です。