bobtail 0.3.0

Generate macro proxies of functions whose tails can be "bobbed" as in cut off
Documentation

bobtail

Generate macro proxies of functions whose tails can be "bobbed" as in cut off.

This crate produces macro proxies of functions whose trailing arguments may be omitted or provided with less boilerplate.

Prototypes

The define! macro generates macro proxies for functions and method prototypes.

Free Functions

fn f(a: u8, b: Option<u8>) -> u8 {
  b.map(|x| x + a).unwrap_or(a)
}

bobtail::define! {
    fn f(a: u8, #[tail] b: Option<u8>);
}
assert_eq!(f(1, Some(2)), 3);    // Call function.
assert_eq!(f!(1), 1);            // Call macro with omission.
#[cfg(feature = "omit-token")]
assert_eq!(f!(1, _), 1);         // Call macro with explicit omission.
assert_eq!(f!(1, 2), 3);         // Pass unwrapped second argument.
# assert_eq!(f!(1, Some(2)), 3); // Pass wrapped second argument.

Generated Macro Rules

The bobtail::define! produces macro rules that might have looked like this code.

macro_rules! f {
    ($a:expr) => {
        f($a, None)
    };
    ($a:expr, $b:expr) => {
        f($a, Some($b))
    };
}

But bobtail::define! can handle the following case, which the above macro can not.

assert_eq!(f!(1, Some(2)), 3);   // Pass wrapped second argument.

How? Because instead of being restricted to Option, an ommitable parameter can be any type that implements Default and From<T>. What bobtail::define! actually produces is this:

macro_rules! f {
    ($a:expr) => {
        f($a, Default::default())
    };
    ($a:expr, $b:expr) => {
        f($a, From::from($b))
    };
}

From<T> is not only more flexible, but it permits one to use Some(2) above because there is a blanket implementation for From<T> for all T, which is an identity function.

Methods

Methods with a &self, &mut self, or self expect the receiver as the first argument to the macro.

struct A;

impl A {
    fn b(&self, a: u8, b: Option<u8>) -> u8 {
        b.map(|x| x + a).unwrap_or(a)
    }
    fn c(self, a: u8) -> u8 {
        a
    }
}

bobtail::define! {
    fn b(&self, a: u8, #[bobtail::tail] b: Option<u8>) -> u8;
    // Name the macro explicitly.
    c_macro => fn c(self, #[tail] a: u8); // Return type can be omitted.
}
let a = A;

assert_eq!(a.b(1, Some(2)), 3);   // Call function.

assert_eq!(b!(a, 1, Some(2)), 3); // Call macro.
assert_eq!(b!(a, 1, 2), 3);       // Omit `Some`.
assert_eq!(b!(a, 1), 1);          // Omit second argument.
#[cfg(feature = "omit-token")]
assert_eq!(b!(a, 1, _), 1);       // Explicitly omit second argument.
assert_eq!(c_macro!(a, 4), 4);    // Consume self.

let a = A;
assert_eq!(c_macro!(a), 0);       // Any `Default` will do.

Attributes

One can also generate macro proxies with attributes.

Free Functions

#[bobtail::bob]
fn f(a: u8, #[tail] b: Option<u8>) -> u8 {
  b.map(|x| x + a).unwrap_or(a)
}

assert_eq!(f(1, Some(2)), 3);    // Call function.
assert_eq!(f!(1), 1);            // Call macro with omission.
#[cfg(feature = "omit-token")]
assert_eq!(f!(1, _), 1);         // Call macro with explicit omission.
assert_eq!(f!(1, 2), 3);         // Pass unwrapped second argument.
# assert_eq!(f!(1, Some(2)), 3); // Pass wrapped second argument.

Methods

Methods with a &self, &mut self, or self expect the receiver as the first argument to the macro proxy.

struct A;

#[bobtail::block]
impl A {
    #[bobtail::bob]
    fn b(&self, a: u8, #[bobtail::tail] b: Option<u8>) -> u8 {
        b.map(|x| x + a).unwrap_or(a)
    }
    #[bob(c_macro)] // Name the macro explicitly.
    fn c(self, #[tail] a: u8) -> u8 {
        a
    }
}

let a = A;
assert_eq!(a.b(1, Some(2)), 3);   // Call function.

assert_eq!(b!(a, 1, Some(2)), 3); // Call macro.
assert_eq!(b!(a, 1, 2), 3);       // Omit `Some`.
assert_eq!(b!(a, 1), 1);          // Omit second argument.
#[cfg(feature = "omit-token")]
assert_eq!(b!(a, 1, _), 1);       // Explicitly omit second argument.
assert_eq!(c_macro!(a, 4), 4);    // Consume self.

let a = A;
assert_eq!(c_macro!(a), 0);       // Any `Default` will do.

Motivation and Justification

This crate was inspired by my work on Nano-9, a Pico-8 compatibility layer for Bevy. Pico-8's Lua API has many arguments that are often omitted. Consider Pico-8's text drawing function print.

-- print(str, [x,] [y,] [color])
print("hello world")
-- No x? No y? No problem.

Nano-9 provides the Lua API as-is, but it also provides a Pico-8-like API in Rust for which the above looks like this:

// print(str, vec2, color, /* Nano-9 extensions: */ font_size, font_index)
pico8.print("hello world", None, None, None, None).unwrap();

The aim of this crate is to offer an API on the Rust side that is not so verbose.

print!(pico8, "hello world").unwrap();

A Caution

The above are my reasons for creating this crate, but that does not mean I wholeheartly endorse this kind of positional, omittable, API design. If I were not constrained by Pico-8's initial design and wanting to bear a strong resemblance to it, I would consider using structs expressively as named and omittable arguments potentially using other crates like bon, typed-builder, and derive_builder.

Features

bobtail generates self-contained and straightforward to inspect macros, but one cannot use the underscore '_' to omit values without using the omit-token feature.

omit-token - Enable _ placeholder syntax

If one wants to use _ as a placeholder for default values anywhere in the tail arguments, enable the omit-token feature:

[dependencies]
bobtail = { version = "0.3", features = ["omit-token"] }

With this feature enabled, one can write:

assert_eq!(f!(1, _), 1);         // Explicitly use default for second arg
assert_eq!(f!(1, _, 3), ...);    // Use default for second, explicit third

Without the feature, one would need to pass explicit default values:

assert_eq!(f!(1, None, 0), 1);           // Explicitly pass `None`
assert_eq!(f!(1, Default::default(), 0), // Explicitly pass default
           1);

The trade-off is that omit-token uses a recursive helper macro internally, which makes macro expansion slightly more complex.

Advanced Features

Macro Visibility

By default, the generated macro has the same visibility as the function. One can override this with explicit visibility:

For #[bob] attribute:

#[bobtail::bob(pub(crate) my_macro)]  // Define visibility and macro name.
#[bobtail::bob(pub(crate))]           // Define macro visibility.
#[bobtail::bob(pub(self))]            // Define private macro.

For define! macro:

bobtail::define! {
    pub(crate) my_macro => fn foo(a, #[tail] b);  // Define visibility and macro name.
    pub(crate) => fn bar(a, #[tail] b);           // Define macro visibility.
    pub(self) => fn bar(a, #[tail] b);            // Define private macro.
}
  • pub visibility adds #[macro_export].
  • pub(crate) and pub(in path) use re-exports.
  • pub(self) creates a private macro even for a public function.

Macro Attributes

Add custom attributes to generated macros using #[bobtail::macro_attrs(...)]:

#[bobtail::macro_attrs(doc(hidden))]
#[bobtail::bob]
pub fn foo(a: u8, #[tail] b: Option<u8>) -> u8 {
    b.map(|x| x + a).unwrap_or(a)
}

For define!, use outer attributes directly:

bobtail::define! {
    #[doc(hidden)]
    fn foo(a, #[tail] b);
}

Typeless Parameters in define!

Types are optional in define! since only parameter counts and #[tail] markers matter for macro generation:

bobtail::define! {
    fn foo(a, #[tail] b);  // No types needed
}

This is equivalent to:

bobtail::define! {
    fn foo(a: u8, #[tail] b: Option<u8>) -> u8;
}

Install

Add bobtail to a project with the following command:

cargo add bobtail

Or add these lines to the Cargo.toml file:

[dependencies]
bobtail = "0.3"

License

This crate is licensed under the MIT License or the Apache License 2.0.