Rustの可変長引数関数とHListの話

この記事は 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を付与する必要があります。

別の実現方法としてはマクロを利用する案もあり、以下のようなマクロで実現できるでしょう。以下はargarg2を処理し、再帰的に可変長引数に相当する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はHNilHConsで表現されます。実装は以下のようになっています。

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>>>となります。このようにHNilHConsを組み合わせて、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];に注目してください。&stri32f64boolという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;
}

HNilHConsに対して、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>ArgsHCons<T, Args>としています。TheadArgstailです。headはそのままStringに変換したいため、ジェネリック境界としてToStringを課しています。tail再帰的に処理を進めるため、PrintList: Printer<Args>とし、printを呼び出します。この処理はHNilに到達するまで繰り返されます。

このようにして、HNilHConsに対して、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も扱えるようなので、折を見て遊んでみようと思います。

References