この記事は Rust Advent Calendar 2020 の5日目の記事です。
背景
RFCs#2137にあるようにRustでは、可変長引数関数を直接的に書くことはできません。とはいえ全くできないわけではありません。C言語から可変長引数関数呼び出しを実現するため、stub関数を記述することは可能です。例えば、以下のような関数をRustで実装します。
pub unsafe extern "C" fn func(arg: T, arg2: U, mut args: ...) { // do something }
このような関数はuse extern "C"
の中で使われ、unsafe
を付与する必要があります。
別の実現方法としてはマクロを利用する案もあり、以下のようなマクロで実現できるでしょう。以下はarg
とarg2
を処理し、再帰的に可変長引数に相当するargs
を処理する例です。
macro_rules! func { ( $arg:expr, $arg2:expr, $($args:expr), + ) => { do_something_for_arg($arg); do_something_for_arg1($arg2); func!(@inner $($args), +); }; (@inner $tail:expr ) => { $tail }; (@inner $head:expr, $($cons:expr), + ) => { do_something_args($head); func!(@inner $($cons), +) }; }
HListについて
今回は可変長引数関数を実現する方法の一つとして、HListを紹介します。HlistはHeterogenous Listの略で、値に異なる型を取れるリストです。例えば、[1, "a", true]
のような[u32, &str, bool]
型の値を持つことができます。
似たようなデータ構造としてはVec<T>
やタプルが考えられますが、Vec<T>
は同一の型T
を持つ点で異なります。一方、タプルは(u32, &str, bool)
のように異なる型を持つこともできますが、HListを用いることでコンパイル時まで要素の数を決めないでいられます。
frunk
RustでHListを扱うために、frunk crateを利用します。frunkはgeneric functional programmingをRustで扱えるようにするためのcrateで、以下をサポートしています。
- HLists (heterogeneously-typed lists)
- LabelledGeneric, and Generic
- Coproduct
- Validated (accumulator for Result)
- Semigroup
- Monoid
今回は上記の中から、HListを利用します。frunk crateのHListはHNil
とHCons
で表現されます。実装は以下のようになっています。
pub struct HNil; pub struct HCons<H, T> { pub head: H, pub tail: T, }
frunk crateにはhlist
マクロが定義されており、hlist![1, "a", true]
の返り値の型はHCons<i32, HCons<&str, HCons<bool, HNil>>>
となります。このようにHNil
とHCons
を組み合わせて、HListを表現します。
作るもの
可変長引数を取る関数の代表的な例として、print
関数を実装します。
使い方をみたほうがイメージが湧きやすいと思うので、先にテストを書きます。
#[test] fn my_print() { use frunk::hlist; use super::Printer; let args = hlist!["hello ", "world. ", 1, " == ", 2.0, " is ", false]; let got = args.print(); assert_eq!(got, "hello world. 1 == 2 is false"); }
let args = hlist!["hello ", "world. ", 1, " == ", 2.0, " is ", false];
に注目してください。&str
、i32
、f64
、bool
という4つの異なる型を含んだListとなっています。本当はargs
を引数に取るような可変長引数関数を直接実装したかったのですが、今回は擬似的にargs
に対するメソッドとして実装します。Implementing a Type-safe printf in Rustでは、プレースホルダをFVar
として表現し、引数としてHListを受け取るformat
関数を実装しています。ぜひこちらもご覧ください。
実装
まず、Printer
traitを定義します。
String
を返すようなprint
関数を持ちます。
pub trait Printer<Args> { fn print(&self) -> String; }
HNil
とHCons
に対して、Printer
traitを満たすように、print
関数を実装していきます。
まずは、HNil
から実装します。
impl Printer<HNil> for HNil { fn print(&self) -> String { "".to_string() } }
HNil
となった時点でHListは末尾に到達しているので、空文字を返します。シンプルですね。
一方、HCons
は少し複雑です。
impl<T, Args, PrintList> Printer<HCons<T, Args>> for HCons<T, PrintList> where PrintList: Printer<Args>, T: ToString, { fn print(&self) -> String { self.head.to_string() + &self.tail.print() } }
Printer<Args>
のArgs
をHCons<T, Args>
としています。T
がhead
、Args
がtail
です。head
はそのままString
に変換したいため、ジェネリック境界としてToString
を課しています。tail
は再帰的に処理を進めるため、PrintList: Printer<Args>
とし、print
を呼び出します。この処理はHNil
に到達するまで繰り返されます。
このようにして、HNil
とHCons
に対して、print
関数を実装しました。
動作確認
改めて、今回実装したコード全体は以下のようになります。
use frunk::hlist::{HNil, HCons}; pub trait Printer<Args> { fn print(&self) -> String; } impl Printer<HNil> for HNil { fn print(&self) -> String { "".to_string() } } impl<T, Args, PrintList> Printer<HCons<T, Args>> for HCons<T, PrintList> where PrintList: Printer<Args>, T: ToString, { fn print(&self) -> String { self.head.to_string() + &self.tail.print() } } #[cfg(test)] mod tests { #[test] fn my_print() { use frunk::hlist; use super::Printer; let args = hlist!["hello ", "world. ", 1, " == ", 2.0, " is ", false]; let got = args.print(); assert_eq!(got, "hello world. 1 == 2 is false"); } }
テストの実行結果です。
❯ cargo test Compiling rust-variadic-artgument v0.1.0 (/Users/cipepser/work/github.com/cipepser/rust-variadic-artgument) Finished test [unoptimized + debuginfo] target(s) in 0.47s Running target/debug/deps/rust_variadic_artgument-3442cb5a09929580 running 1 test test tests::my_print ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests rust-variadic-artgument running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
いい感じですね。
最後に
可変長引数関数の話から始まり、HListでprint
関数を実装しました。
今回記事を執筆するにあたり、frunc crateの存在を初めて知りました。CoproductやSemigroup、Monoidも扱えるようなので、折を見て遊んでみようと思います。