💤

rustでデフォルト引数を作る

2023/12/29に公開

デフォルト引数はプロダクトコードではあんまり使いたくないですが、unitテストなんかでは結構重宝しています。しかしrustにはそれらしい機能が無いっぽいのでmacroを使って自前でやる方法考えました。

名前付き引数を作る

今回のお題としては構造体の指定したメンバ以外はデフォルトの値を設定するような関数を作ってみたいと思います。まずは名前付き引数を使うための構文を作ってみます。段階的に作っていきたいのでprintlnを使いながら挙動を確かめたいと思います。以下は実際に実装してみたmacroとそれを動作させるためのmain関数になります。

struct Test {
    test1: i64,
    test2: i64,
    test3: i64,
}

macro_rules! create_test {
    ($key1:ident: $value1:expr, $key2:ident: $value2:expr) => {
        let mut t = Test {test1: 10, test2: 11, test3: 12};
        t.$key1 = $value1;
        t.$key2 = $value2;
        println!("t.test1 = {}", t.test1);
        println!("t.test2 = {}", t.test2);
        println!("t.test3 = {}", t.test3);
    };
}

fn main() {
    create_test!(test1: 1, test2: 2);
}

// 実行結果
// t.test1 = 1
// t.test2 = 2
// t.test3 = 12

上記のコードはcreate_testというTest構造体を生成するためのmacroを実装しています。rustのmacroはmacro_rulesで定義することができます。($key1:ident: $value1:expr, $key2:ident: $value2:expr)の部分はこのmacroの引数に当たる部分になっています(正確にはmatcherのようなものらしいです)。ここで大事なのが$key1:ident:$value1:exprです。$key1という第一引数には識別子(ident)が格納されます。識別子とは関数や変数などの名前が該当します。次に:ですが、これは引数の区切り文字となります。macroではカンマ以外の文字列を区切り文字にできます。最後に$value1:exprは式(expr)を$valueに格納することができます。これらを使ってmacro内部で定義している構造体を格納している変数tに対して動的にメンバにアクセスして値を代入し名前付き引数を実現できるわけです。

順不同、個数不定の引数を作る

先程のコードだと固定数の引数となってしまうため、test1だけに値を設定したい場合や、すべてのメンバに値を設定したい場合に対応できません。なので先程のmacroの引数を順不同で、かつ、個数も不定にできるようにします。rustのmacroは正規表現のように繰り返しの構文を作ることが可能です。実際に実装してみると以下のようになります。

struct Test {
    test1: i64,
    test2: i64,
    test3: i64,
}

macro_rules! create_test {
    ($($key:ident: $value:expr),*) => {
        let mut t = Test {test1: 10, test2: 11, test3: 12};
        $(
            t.$key = $value;
        )*
        println!("t.test1 = {}", t.test1);
        println!("t.test2 = {}", t.test2);
        println!("t.test3 = {}", t.test3);
    };
}

fn main() {
    println!("------");
    create_test!(test1: 1);
    println!("------");
    create_test!(test1: 1, test2: 2);
    println!("------");
    create_test!(test2: 2, test3: 3);
    println!("------");
    create_test!(test2: 2, test1: 1, test3: 3);
}

// 実行結果
// ------
// t.test1 = 1
// t.test2 = 11
// t.test3 = 12
// ------
// t.test1 = 1
// t.test2 = 2
// t.test3 = 12
// ------
// t.test1 = 10
// t.test2 = 2
// t.test3 = 3
// ------
// t.test1 = 1
// t.test2 = 2
// t.test3 = 3

引数の$(...),*の構文によって、引数のパターンに0個以上のマッチを行うことができます。これで引数が可変になります。そして$(...)*の構文によって構造体のメンバへの代入処理を引数の数だけ繰り返し行うことができます。

戻り値を作る

ここまででデフォルト値を持った引数は作れたので最後にこれを関数として動作させられるように戻り値を定義します。しかしmacro自体は関数ではないので戻り値という概念がそもそもありません。そこで今回はブロックを利用します。rustではブロックも式の一種となります。ブロック内の最後の式がブロック自身を評価したときの戻り値となります。具体的には以下のようなコードがかけるということです。

fn main() {
    let i = {
        3
    };
    println!("i = {}", i);
}

// 実行結果
// i = 3

これを応用することでmacroを関数のように振る舞わせることができます。

struct Test {
    test1: i64,
    test2: i64,
    test3: i64,
}

macro_rules! create_test {
    ($($key:ident: $value:expr),*) => {
        {
            let mut t = Test {test1: 10, test2: 11, test3: 12};
            $(
                t.$key = $value;
            )*
            t
        }
    };
}

fn main() {
    let t1 = create_test!(test1: 1, test2: 2);
    println!("Test.test1 = {}", t1.test1);
    println!("Test.test2 = {}", t1.test2);
    println!("Test.test3 = {}", t1.test3);
    println!("-----");
    let t2 = create_test!(test2: 2, test3: 3);
    println!("Test.test1 = {}", t2.test1);
    println!("Test.test2 = {}", t2.test2);
    println!("Test.test3 = {}", t2.test3);
}

// 実行結果
// Test.test1 = 1
// Test.test2 = 2
// Test.test3 = 12
// -----
// Test.test1 = 10
// Test.test2 = 2
// Test.test3 = 3

ブロックを使って戻り値として構造体Testを返せるようにすることでデフォルト値を持ったデータ生成関数を実装できました。

0個引数

先程のコードは引数を以下のように指定しなくても動作します。しかしコンパイル時にワーニングが出てしまってあんまりよろしくありません。

fn main() {
    let t = create_test!();
    println!("Test.test1 = {}", t.test1);
    println!("Test.test2 = {}", t.test2);
    println!("Test.test3 = {}", t.test3);
}

// コンパイル結果
// let mut t = Test {test1: 10, test2: 11, test3: 12};
//     ----^
//     |
//     help: remove this `mut`

これは引数を指定しなかった場合はmacro内部で定義している変数がmutである必要がなくなってしまうからですね。ワーニングを出さないために0個の引数のときのパターンを作成してしまいましょう。実はrustは同じmacroを引数のパターンに応じてオーバーロードすることができます。具体的には以下のようにすることで0個のパターンを別途実装してワーニングを避けることができます。ついでに複数の引数がある場合のパターンを*から+に変更して1つ以上とマッチするようにしています。

struct Test {
    test1: i64,
    test2: i64,
    test3: i64,
}

macro_rules! create_test {
    () => {
        {
            Test {test1: 10, test2: 11, test3: 12}
        }
    };

    ($($key:ident: $value:expr),+) => {
        {
            let mut t = Test {test1: 10, test2: 11, test3: 12};
            $(
                t.$key = $value;
            )+
            t
        }
    };
}

fn main() {
    let t1 = create_test!();
    println!("Test.test1 = {}", t1.test1);
    println!("Test.test2 = {}", t1.test2);
    println!("Test.test3 = {}", t1.test3);
    println!("------");
    let t2 = create_test!(test2: 2, test3: 3);
    println!("Test.test1 = {}", t2.test1);
    println!("Test.test2 = {}", t2.test2);
    println!("Test.test3 = {}", t2.test3);
}

// 実行結果
// Test.test1 = 10
// Test.test2 = 11
// Test.test3 = 12
// ------
// Test.test1 = 10
// Test.test2 = 2
// Test.test3 = 3

まとめ

macroを使えば一応デフォルト引数は作れますすごい。メタプログラミングは面白いですね。でもメタプログラミングはややこしいので用法用量は守って使いましょう。

Discussion