Search code examples
rustrust-macros

How to initialize a struct with a series of arguments


In many languages, a common constructor idiom is to initialize values of an object using syntax like this pseudocode:

constructor Foo(args...) {
    for arg {
        object.arg = arg
    }
}

Rust at first seems to be no exception. Many impl for a struct include a constructor named new to zip an ordered series of arguments onto the fields of the struct:

struct Circle {
    x: i32,
    y: i32,
    radius: i32,
}

impl Circle {
    fn new(x: i32, y: i32, radius: i32) -> Circle {
        Circle { x: x, y: y, radius: radius }
    }
}

Doing this with a macro might look like zip!(Circle, 52, 32, 5). It would zip the values, in order, onto the fields of Circle. Both zip!(Circle, 52, 32) or zip!(Circle, 52, 32, 5, 100) would present issues, but a macro like this would be a very flexible way to push values onto a new instance of any struct without so much boilerplate.

Is there an idiomatic way to simplify this boilerplate? How is it possible to map a series of ordered arguments onto each field of a struct without explicitly writing the boilerplate code to do so?


Solution

  • This is not possible with a macro for a very simple reason: a macro cannot conjure the field names out of thin air.

    The simplest solution, if you are comfortable exposing the details of your type, is making the fields public:

    struct Circle {
        pub x: i32,
        pub y: i32,
        pub radius: i32,
    }
    
    fn main() {
        let circle = Circle { x: 3, y: 4, radius: 5 };
    }
    

    That is, there is no need to have a constructor, it works perfectly fine without one.

    After all, if the constructor does nothing else than passing on the values, the constructor itself is rather pointless, isn't it?

    If you wish to offer a shorter initialization syntax, you can for example:

    use std::convert::From;
    
    impl From<(i32, i32, i32)> for Circle {
        fn from(t: (i32, i32, i32)) -> Circle {
            Circle { x: t.0, y: t.1, radius: t.2 }
        }
    }
    
    fn main() {
        let circle: Circle = (3, 4, 5).into();
    }
    

    And normally, type inference should relieve you from having to spell out : Circle.

    I would note however that this is much more error-prone, as swapping two of the arguments without noticing is much easier. You may want to stick to explicit names, or instead introduce explicit types.