Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a read-only struct without boilerplate code?

Despite the fact that Rust has absorbed many good modern programming ideas, it looks like one very basic feature is not presented.

The modern (pseudo-)functional code is based on a large number of classes of the following kind:

pub struct NamedTuple {
    a: i8,
    b: char,
}
impl NamedTuple {
    fn new(a: i8, b: char) -> NamedTuple {
        NamedTuple { a: a, b: b }
    }
    fn a(&self) -> i8 {
        self.a
    }
    fn b(&self) -> char {
        self.b
    }
}

As you can see, there is a lot of boilerplate code here. Is there really no way to describe such types compactly, without a boilerplate code?

like image 527
warlock Avatar asked Jun 19 '18 13:06

warlock


Video Answer


2 Answers

When you have boilerplate, think macros:

macro_rules! ro {
    (
        pub struct $name:ident {
            $($fname:ident : $ftype:ty),*
        }
    ) => {
        pub struct $name {
            $($fname : $ftype),*
        }

        impl $name {
            fn new($($fname : $ftype),*) -> $name {
                $name { $($fname),* }
            }

            $(fn $fname(&self) -> $ftype {
                self.$fname
            })*
        }
    }
}

ro!(pub struct NamedTuple {
    a: i8,
    b: char
});

fn main() {
    let n = NamedTuple::new(42, 'c');
    println!("{}", n.a());
    println!("{}", n.b());
}

This is a basic macro and could be extended to handle specifying visibility as well as attributes / documentation on the struct and the fields.

I'd challenge that you have as much boilerplate as you think you do. For example, you only show Copy types. As soon as you add a String or a Vec to your structs, this will fall apart and you need to either return a reference or take self.


Editorially, I don't think this is good or idiomatic Rust code. If you have a value type where people need to dig into it, just make the fields public:

pub struct NamedTuple {
    pub a: i8,
    pub b: char,
}

fn main() {
    let n = NamedTuple { a: 42, b: 'c' };
    println!("{}", n.a);
    println!("{}", n.b);
}

Existing Rust features prevent most of the problems that getter methods attempt to solve in the first place.

Variable binding-based mutability

n.a = 43;
error[E0594]: cannot assign to field `n.a` of immutable binding

The rules of references

struct Something;

impl Something {
    fn value(&self) -> &NamedTuple { /* ... */ }
}

fn main() {
    let s = Something;
    let n = s.value();
    n.a = 43;
}
error[E0594]: cannot assign to field `n.a` of immutable binding

If you've transferred ownership of a value type to someone else, who cares if they change it?

Note that I'm making a distinction about value types as described by Growing Object-Oriented Software Guided by Tests, which they distinguish from objects. Objects should not have exposed internals.

like image 97
Shepmaster Avatar answered Sep 28 '22 17:09

Shepmaster


Rust doesn't offer a built-in way to generate getters. However, there are multiple Rust features that can be used to tackle boilerplate code! The two most important ones for your question:

  • Custom Derives via #[derive(...)] attribute
  • Macros by example via macro_rules! (see @Shepmaster's answer on how to use those to solve your problem)

I think the best way to avoid boilerplate code like this is to use custom derives. This allows you to add a #[derive(...)] attribute to your type and generate these getters at compile time.

There is already a crate that offers exactly this: derive-getters. It works like this:

#[derive(Getters)]
pub struct NamedTuple {
    a: i8,
    b: char,
}

There is also getset, but it has two problems: getset should have derive in its crate name, but more importantly, it encourages the "getters & setters for everything" anti pattern by offering to also generate setters which don't perform any checks.


Finally, you might want to consider rethinking your approach to programming in Rust. Honestly, from my experience, "getter boilerplate" is hardly a problem. Sure, sometimes you need to write getters, but not "a large number" of them.

Mutability is also not unidiomatic in Rust. Rust is a multi paradigm language, supporting many styles of programming. Idiomatic Rust uses the most useful paradigm for each situation. Completely avoiding mutation might not be the best way to program in Rust. Furthermore, avoiding mutability is not only achieved by providing getters for your fields -- binding and reference mutability is far more important!

So, use read-only access to fields where it's useful, but not everywhere.

like image 21
Lukas Kalbertodt Avatar answered Sep 28 '22 17:09

Lukas Kalbertodt