Rustで+++を実現する


はじめに

JavaScriptで+++は許されない+ ++は許される
C/C++ では+++は許される
Rustで+++は許されない+ ++も許されない

なんというか「できません」と言われるとやりたくなりますよね。
しかもちょうどいいタイミングで、関数呼び出し形式の手続きマクロ(のexpressionでの呼び出し)が安定化されるようなので、これを使って +++ を実装してみました。

ちなみに、ここで安定化される、と言っているのは

の"Function like procedural macros can now be used in expression, pattern, and statement positions."です。
これまで関数呼び出し形式の手続きマクロは使用できる場所がかなり限られていたのですが、この安定化により広範囲に使用できるようになります。

この更新は7/16(日本時間だと7/17あたり?)にリリース予定のRust 1.45.0で反映される予定です。

手続きマクロ

手続きマクロとはRustのソースコードを入力としてRustのソースコードを出力するようなRustのプログラムです。
そのため文字列置換ベースのマクロでは難しい複雑な処理も可能です。
しかも、入力は厳密にはRustの文法に従っている必要はありません。
そのため +++ のようなRustの文法上許されない記述も、手続きマクロの入力としてなら受理されるのです。

というわけで

use cont_ops::cont_ops;

fn main() {
    let mut a = 0;
    let b = 1;
    let c = cont_ops!(a +++ b);
    dbg!(a, b, c);
}

という記述を可能にする cont_ops! を実装します。

cont_ops!

以下が cont_ops! の実装です。
かなり雑なパースですが、2連続した +-を判定して取り除き、あとでインクリメント・デクリメントするコードを挿入しています。

use proc_macro2::{TokenStream, TokenTree};
use quote::quote;
use std::iter::FromIterator;

#[proc_macro]
pub fn cont_ops(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input: TokenStream = input.into();

    let mut ret: Vec<TokenTree> = Vec::new();
    let mut plus = Vec::new();
    let mut minus = Vec::new();
    let mut ident = None;
    let mut plus_idents = Vec::new();
    let mut minus_idents = Vec::new();

    for token in input {
        match &token {
            TokenTree::Punct(x) if x.as_char() == '+' => plus.push(x.clone()),
            TokenTree::Punct(x) if x.as_char() == '-' => minus.push(x.clone()),
            x => {
                for x in &plus {
                    ret.push(TokenTree::Punct(x.clone()));
                }
                for x in &minus {
                    ret.push(TokenTree::Punct(x.clone()));
                }
                plus.clear();
                minus.clear();
                ret.push(x.clone());
                if let TokenTree::Ident(x) = x {
                    ident = Some(x.clone());
                }
            }
        }
        if plus.len() == 2 {
            plus.clear();
            if let Some(ref x) = ident {
                plus_idents.push(TokenTree::Ident(x.clone()));
            }
        }
        if minus.len() == 2 {
            minus.clear();
            if let Some(ref x) = ident {
                minus_idents.push(TokenTree::Ident(x.clone()));
            }
        }
    }
    let ret: TokenStream = TokenStream::from_iter(ret.into_iter()).into();
    let plus_idents = plus_idents.into_iter();
    let minus_idents = minus_idents.into_iter();

    let gen = quote! {
        {
            let tmp = #ret;
            #(
                #plus_idents += 1;
            )*
            #(
                #minus_idents -= 1;
            )*
            tmp
        }
    };

    gen.into()
}

使ってみる

というわけで冒頭の

use cont_ops::cont_ops;

fn main() {
    let mut a = 0;
    let b = 1;
    let c = cont_ops!(a +++ b);
    dbg!(a, b, c);
}

を実行すると

$ cargo +beta run
[src/main.rs:7] a = 1
[src/main.rs:7] b = 1
[src/main.rs:7] c = 1

となります。
(1.45.0がリリースされれば +beta は不要になるはずです)

この ++--は(自分でそのようにパースしているので当然といえば当然ですが)いくらでもつなげることができます。

use cont_ops::cont_ops;

fn main() {
    let mut a = 0;
    let c = cont_ops!(a ++++--+ b --++++--++- 1);
    dbg!(a, b, c);
}

マクロを展開した結果を見てみると以下のようになっています。

$ cargo expand
    let c = {
        let tmp = a + b - 1;
        a += 1;
        a += 1;
        b += 1;
        b += 1;
        b += 1;
        a -= 1;
        b -= 1;
        b -= 1;
        tmp
    };

まとめ

Rustで +++を実現する手続きマクロを実装してみました。
手続きマクロは非常に強力で、うまく使えばいろいろと面白いことが可能です。
ただし、この記事のようにやりすぎると訳が分からなくなってくるので、使いすぎには注意しましょう。