Crate variadiz

source ·
Expand description

Variadic function support for Rust.

§Install

cargo add variadiz

§Example

use variadiz::*;

#[variadic]
fn print<T, U>(mut counter: usize, non_variadic: T, others: Option<U>)
where
    T: std::fmt::Display,
    U: std::fmt::Debug,
{
    #[va_expand_ref(mut counter: usize)]
    {
        println!("{counter}: {:?}", others);
        *counter += 1;
    }
    #[va_expand_mut]
    {
        others.take();
    }
    #[va_expand(mut counter: usize, non_variadic: T)]
    {
        println!("[{non_variadic}] {counter}: {:?}", others);
        *counter += 1;
    }
}

print(
    0,
    20240429,
    va_args!(Some("hello"), Some(vec![1, 2, 3]), Some('e')),
);

Outputs:

0: Some("hello")
1: Some([1, 2, 3])
2: Some('e')
[20240429] 3: None
[20240429] 4: None
[20240429] 5: None

As methods:

use std::fmt::Debug;
use variadiz::*;

struct Person<'a, T>
where
    T: Debug,
{
    name: &'a str,
    age: u32,
    tags: Vec<T>,
}

#[variadic_impl]
impl<'a, T> Person<'a, T>
where
    T: Debug,
{
    // Non-variadic method
    fn hello(&self) -> &'static str {
        "hello"
    }

    #[variadic]
    fn grow_up<U>(&mut self, others: U)
    where
        U: std::fmt::Debug,
    {
        #[va_expand(hello: &str, who: &str, mut age: u32, tags: Vec<T>)]
        #[va_bind(hello = self.hello())]
        #[va_bind(who = self.name)]
        #[va_bind(age = self.age)]
        #[va_bind(tags = self.tags)]
        {
            println!("{hello}, {who} is {age} years old,");
            println!("\tthey are {tags:?},");
            println!("\tand do not forget {others:?}");
            *self.age += 1;
        }
    }
}

let mut person = Person {
    name: "John",
    age: 16,
    tags: vec!["smart", "awesome"],
};
person.grow_up(va_args!("hell", Some(62), 0.96));

Outputs:

hello, John is 16 years old,
        they are ["smart", "awesome"],
        and do not forget "hell"
hello, John is 17 years old,
        they are ["smart", "awesome"],
        and do not forget Some(62)
hello, John is 18 years old,
        they are ["smart", "awesome"],
        and do not forget 0.96

§Details

§The #[variadic] attribute

This attribute macro always takes the last declared generic type and the last parameter for variadic, i.e.:

// The generic type `U` and the parameter `others` are used for variadic.
#[variadic]
fn print<T, U>(counter: usize, non_variadic: T, others: Option<U>) {
    todo!()
}

NOTE: It is undefined behavior to use the variadic generic type elsewhere, including be used in bounds of other generic types.

However, conversely the variadic generic type can be bound by other generic types:

#[variadic]
fn print<T, U>(counter: usize, non_variadic: T, others: Option<U>)
where
    // T: From<U>,  // Bad, the behavior is undefined
    U: From<T>,     // OK
{
    todo!()
}

§Expand variadic parameter pack

To expand the variadic parameter pack, you need to add a #[va_expand] attribute on a block:

#[va_expand]
{
    todo!()
}

Anyway, #[va_expand] always consumes all variadic parameters, even if them are bound by Copy.

Instead, using #[va_expand_ref] and #[va_expand_mut], you will get an immutable reference or a mutable reference to each variadic parameter, allowing you to expand the variadic parameter pack multiple times.

It should be noted that the expansion block behaves like a function body rather than a closure body - it cannot capture variables from the context automatically.

To capture context variables, you must declare them like how you declare function parameters:

#[variadic]
fn print<T>(x: i32, y: &str, others: T) {
    #[va_expand(x: i32, y: &str)]
    {
        todo!()
    }
}

NOTE: Since the captured variables must be usable multiple times after being expanded, we can only use their references anyway. So when you declare to capture x, the &x is actually captured. You can add a mut before the captured variable to indicate capturing its mutable reference.

For example:

use variadiz::*;

#[variadic]
fn print<T, U>(mut counter: usize, non_variadic: T, others: Option<U>)
where
    T: std::fmt::Display,
    U: std::fmt::Debug,
{
    // Capture `counter` by mutable reference.
    #[va_expand_ref(mut counter: usize)]
    {
        println!("{counter}: {:?}", others);
        // `counter` here is actually `&mut usize`,
        //  a mutable reference to the original `counter`.
        *counter += 1;
    }
    #[va_expand_mut]
    {
        others.take();
    }
    // Capture `counter` by mutable reference,
    // then capture `non_variadic` by immutable reference.
    #[va_expand(mut counter: usize, non_variadic: T)]
    {
        println!("[{non_variadic}] {counter}: {:?}", others);
        *counter += 1;
    }
}

print(
    0,
    20240429,
    va_args!(Some("hello"), Some(vec![1, 2, 3]), Some('e')),
);

Another example:

use variadiz::*;

#[variadic]
fn collect<T, U>(mut collector: Vec<T>, others: Option<U>) -> Vec<T>
where
    U: Into<T>, // `U` can be bound by `T`, but not vice versa.
{
    // `collector` is actually `&mut Vec<T>`
    #[va_expand(mut collector: Vec<T>)]
    {
        if let Some(item) = others {
            // The type `U` is specific to each variadic parameter.
            // `U` outside an expanded block is **undefined**.
            collector.push(<U as Into<T>>::into(item));
        }
    }
    collector
}

let strs = collect(
    vec![String::from("hello")],
    va_args!(Some("world"), None::<std::borrow::Cow<str>>, Some('e')),
);
println!("{:?}", strs);

Outputs:

["hello", "world", "e"]

You can use an expanded block almost anywhere expressions can be used, and it always evaluates to a (). However, there are two places where you cannot use a expanded block: in another expanded block, or in a macro. For examples:

use variadiz::*;

#[variadic]
fn print<T>(mut counter: usize, others: Option<T>)
where
    T: std::fmt::Debug,
{
    let _result = (0..10)
        .map(|i| {
            if i % 2 == 0 {
                #[va_expand_ref(mut counter: usize)]
                {
                    println!("{counter}: {:?}", others);
                    *counter += 1;
                }
                counter
            } else {
                #[va_expand_ref(mut counter: usize)]
                {
                    println!("{counter}: {:?}", others);
                    *counter -= 1;
                }
                10 - counter
            }
        })
        .collect::<Vec<_>>();

    // Expanded block in an expanded block is not allowed.
    // #[va_expand_ref(mut counter: usize)]
    // {
    //     #[va_expand_ref(mut counter: usize)]
    //     {
    //         println!("{counter}: {:?}", others);
    //         *counter += 1;
    //     }
    // }

    // Expanded block in a macro is not allowed.
    // call_macro! {
    //     #[va_expand_ref(mut counter: usize)]
    //     {
    //         println!("{counter}: {:?}", others);
    //         *counter += 1;
    //     }
    //};
}

print(0, va_args!(Some("hello"), Some(vec![1, 2, 3]), Some('e')));

Except for the captured variables, all generic types, and the identifier of the variadic parameter pack, the expanded block cannot interact with the outer code block.

This means, you cannot return a value to the outer by omitting the semicolon of the last statement, nor can you operate on the outer’s control flow (i.e., for, loop, labelled block) via continue and break.

A statement return in an expanded block will only exit the expanded block itself (similar to continue in a loop block) rather than exiting the outer function.

§Bind captured variables

You can use the #[va_bind] attribute to bind a captured variable to another value, for example:

use variadiz::*;

struct Person {
    name: String,
    age: u32,
}

#[variadic]
fn print<T>(mut person: Person, interests: T)
where
    T: std::fmt::Debug,
{
    #[va_expand_ref(who: String, mut age: u32)]
    #[va_bind(who = person.name, age = person.age)]
    {
        println!("{who} is {age} years old");
        println!("And they are interested in {interests:?}");
        println!("then a year passed...");
        *age += 1;
    }

    /// Although you cannot split the `va_expand` attribute,
    /// but you can split the `va_bind` attribute.
    /// The following code is totally equivalent:
    #[va_expand_ref(who: String, mut age: u32)]
    #[va_bind(who = person.name)]
    #[va_bind(age = person.age)]
    {
        println!("{who} is {age} years old");
        println!("And they are interested in {interests:?}");
        println!("then a year passed...");
        *age += 1;
    }
}

let person = Person {
    name: "John".to_string(),
    age: 18,
};
print(person, va_args!("math", (), 114514));

NOTE: Binding a value to a captured variable does NOT move it.

However, you need to be careful about traps in the case of binding variables to r-values (called value expressions in Rust):

use variadiz::*;

#[variadic]
fn count<T>(_others: T) {
    let mut counter = 0;

    // Bind to l-value (called place expression in Rust).
    #[va_expand_ref(mut counter: usize)]
    #[va_bind(counter = counter)] // Default behavior.
    {
        *counter += 1;
    }
    // `counter` is updated.
    assert_eq!(counter, 4);

    counter = 0;

    // Bind to r-value (called value expression in Rust).
    #[va_expand_ref(mut counter: usize)]
    #[va_bind(counter = counter + 1 - 1)]
    {
        *counter += 1;
    }
    // `counter` is NOT updated!
    assert_eq!(counter, 0);
}

count(va_args!(1, "2", 3.0, [4]));

§Call variadic function

It is easy to see from the above example that you should pack all variadic arguments into va_args! macro and pass them as a single argument.

The va_args! macro accepts any number of expressions. Sometimes you may want to annotate the type of each argument, you can use the va_types! macro:

let args: va_types!(Option<&str>, Option<Vec<usize>>, Option<char>) =
    va_args!(Some("hello"), Some(vec![1, 2, 3]), Some('e'));
print(0, 20240429, args);

§Variadic methods support

Due to some implementation details, #[variadic] has to define some trait item outside the variadic function. It conflicts with impl item – we cannot define trait item in a impl item.

To solve this problem, you are required to add the #[variadic_impl] attribute on the impl item to assist moving these trait items out.

For example:

use std::fmt::Debug;
use variadiz::*;

struct Person<'a, T>
where
    T: Debug,
{
    name: &'a str,
    age: u32,
    tags: Vec<T>,
}

#[variadic_impl]
impl<'a, T> Person<'a, T>
where
    T: Debug,
{
    // Non-variadic method
    fn hello(&self) -> &'static str {
        "hello"
    }

    #[variadic]
    fn grow_up<U>(&mut self, others: U)
    where
        U: std::fmt::Debug,
    {
        #[va_expand(hello: &str, who: &str, mut age: u32, tags: Vec<T>)]
        #[va_bind(hello = self.hello())]
        #[va_bind(who = self.name)]
        #[va_bind(age = self.age)]
        #[va_bind(tags = self.tags)]
        {
            println!("{hello}, {who} is {age} years old,");
            println!("\tthey are {tags:?},");
            println!("\tand do not forget {others:?}");
            *self.age += 1;
        }
    }
}

let mut person = Person {
    name: "John",
    age: 16,
    tags: vec!["smart", "awesome"],
};
person.grow_up(va_args!("hell", Some(514), 0.96));

NOTE: The #[variadic] attribute in the #[variadic_impl] item is not really the attribute macro: it is handled directly and will be removed by #[variadic_impl]. Retaining its name as #[variadic] is an ergonomic consideration. This means it is not affected by Rust’s symbol resolution.

§Variadic trait methods support?

To support variadic methods in traits, it requires sharing private bounds between the trait definition and each implementations. There is current no good design for this purpose.

Macros§

  • Create a variadic argument pack.
  • Annotate types for variadic arguments.

Attribute Macros§

  • Define a variadic function.
  • Indicates that there are variadic methods in the impl item.