`op_result` - thin proc macro sugar for std operator trait bounds

Have you ever tried to write generic code to a std::ops contract and been frustrated at the verbosity?

fn compute_nested<T, U, V>(a: T, b: U, c: V) -> <<<T as Add<U>>::Output as Add<V>>::Output
where
    T: Add<U>,
    <<T as Add<U>>::Output: Add<V>
{
    a + b + c
}

Introducing op_result - a thin proc macro language extension to make op trait bounds palatable:

use op_result::op_result;
use op_result::output;


#[op_result]
fn compute_nested<T, U, V>(a: T, b: U, c: V) -> output!(T + U + V)
where
    [(); T + U]:,
    [(); output!(T + U) + V]:,
    // or, equivalently, with "marker trait notation"
    (): IsDefined<{ T + U }>,
    (): IsDefined<{ output!(T + U) + V }>,
{
    a + b + c
}

// we can even assign output types!
fn compute_with_assignment<T, U, V>(a: T, b: U) -> V
where
    [(); T + U = V]:,
{
    a + b
}

op_result introduces two macros:

  • output! transforms an "operator output expression" into associated type syntax, and can be used flexibly in where bounds, generic parameter lists, and return types

  • op_result transforms a generic function, turning "operator bound expressions" (e.g. [(); T + U]:, (): IsDefined<{ T + U }> into trait bound syntax. This can be combined seamlessly with output! to consistently and readably express complex operator bounds for generic functions.

This works with any std::op that has an associated Output type, and comes complete with span manipulation to provide docs on hover.

Happy coding!

Cool, just wanted to say that there is an easier syntax for where bounds

use std::ops::Add;
fn compute_nested<T, U, V>(a: T, b: U, c: V) -> <<T as Add<U>>::Output as Add<V>>::Output
where
    T: Add<U, Output: Add<V>>,
    
{
    a + b + c
}

Maybe this helps with the proc macro

1 Like

Thanks for the tip! We could expand to that format to allow parsing of flat bound expressions involving more than two types. I didn't implement this as-is, because i wanted to avoid "magic" in the translation layer; knowing there's a representation for this is good.

In practice I think you generally want to avoid chaining where the associativity rule actually matters when reading the code, but this would be useful even in situations where that's not the case, e.g. (T * U) + V. I'll add it.

edit: that was easy

1 Like

Got around to consuming this in the examples for my in-the-works units of measure library, to show how to write a dimensionally-generic PIDController. I think it's pretty decent!