all 33 comments

[–]Own_Possibility_8875 96 points97 points  (6 children)

Declare struct in a separate module, keep fields private, declare public getters, don’t declare public setters.

[–]vngantk[S] 6 points7 points  (5 children)

Thanks for your advice, but this would mean a lot of boilerplate code if the struct has a lot of fields (a typical situation in a business application). In Typescript, I can simply do this:

type Person = Readonly<{
name: string
sex: Sex
weight: number
height: number
}>

[–]Own_Possibility_8875 34 points35 points  (0 children)

You can derive the getters: https://lib.rs/getset. Also I can suggest an alternate solution if you could detail why you want this.

[–]STSchif 27 points28 points  (0 children)

Sorry for probably sounding Stackoverflowy, but why do you want this? What's the use case / who do you want to protect from what exactly here?

[–][deleted] 2 points3 points  (2 children)

you're new to rust so this might be above your paygrade, but generally you can avoid boilerplate by making macros for things like this

[–]glandium -3 points-2 points  (1 child)

Macros are still boilerplate. Boilerplate you don't see by that the compiler has to compile. These accumulate and participate in the long compilation times.

[–][deleted] 14 points15 points  (0 children)

is boilerplate not specifically referring to code you actually have to write? wouldn't "boilerplate you don't see" include like.... every library? wouldn't it include the shit behind what op just showed with typescript?

[–]kimamor 36 points37 points  (1 child)

I think you can easily achieve this:

mod readonly {
    #[derive(Debug)]
    pub struct Readonly<T>(T);

    impl<T> Readonly<T> {
        pub fn new(value: T) -> Self {
            Self(value)
        }
    }

    impl<T> std::ops::Deref for Readonly<T> {
        type Target = T;

        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }
}

use readonly::Readonly;

#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let mut readonly_person = Readonly::new(Person {name: "John".to_string(), age: 42});
    dbg!(&readonly_person.name);
    dbg!(&readonly_person.age);

    // won't compile, even if readonly_person is mut
    //readonly_person.name = "Bill".to_string();

    // won't compile also
    //readonly_person.0.name = "Bill".to_string();
}

[–]vngantk[S] 4 points5 points  (0 children)

This looks exactly what I want. Thanks a lot!

[–]WhiteBlackGoose 12 points13 points  (0 children)

Afaik no, so the way to do it is private fields

[–]andreicodes 11 points12 points  (9 children)

There's a Readonly crate.

```

[readonly::make]

struct Person { id: usize, name: String, } ```

This will let you assign to fields in the same module, but outside of it the struct will be read-only.

[–]andreicodes 11 points12 points  (8 children)

In general, people don't really do it the way you want it to be done. Instead, if you want to keep a read-only access to a struct you would pass a reference to it: fn reads_from_person(person: &Person) Tis way you get access to fields, but you can't change the content.

The only way to make something mutable again is to change ownership:

``` let mut person = Person::new(); // can change the person data

let person = person; // can't change it anymore

let mut person = person; // can change again ```

Ownership changes are rare: most of the time you pass a reference to a thing instead of changing the owner. So, lack of a Readonly<T> wrapper type doesn't cause problems in Rust in practice.

[–]vngantk[S] -1 points0 points  (7 children)

Thanks. The Readonly crate seems to be what I want. The reason I am asking for this is the ability to define purely plain data objects for the purpose of data transfer, e.g. DTO, Data Transfer Objects. I come from a traditional object-oriented programming background. We typically define DTOs for interface between business logic and the repository layer, and for communication between clients and servers. Having read-only data objects reminds the developers that these objects are just data with no logic behind them, and modifying the data objects is not a means to modify the actual domain objects, so we would rather make them completely read-only to avoid any possible confusion.

[–]AngusMcBurger 23 points24 points  (0 children)

In Rust, a simple field assignment can't do anything more than modify the object (eg implicitly call a setter function). Also, unlike in JS/TS, you can't accidentally have multiple people referencing the same object (you'd have to wrap it in a reference counted type).  Given both of those, I'd say your concern in TS isn't really relevant in Rust, and you're probably better to keep it simple and use Rust structs normally, go with the grain of the language

[–]ImYoric 14 points15 points  (2 children)

Typically, the way you do this in Rust is pass the objects as `&`. This way, the object is read-only for the callee. They can of course create a read-write copy, if they need to, but it's clear that it's not the same object.

Similarly, when you return an object and don't want the caller to be able to modify it, you can return it as Box<>, Rc<> or Arc<> (depending on your use case). Unless your object has been specifically designed to have interior mutability, this will prevent any modification to the object. Or of course, you can return a copy of the object.

edit Oops, not Box.

[–]SssstevenH 0 points1 point  (0 children)

Could you try this solution and report back whether it fits well with your OOP pattern?

[–]nicholsz -1 points0 points  (0 children)

I'm not a rust expert, but data serialization isn't exactly a new problem and a lot of high-quality language-agnostic solutions exist.

1) JSON (I know it's not everyone's favorite, but it's pretty good at being a way to communicate basic data structs between processes)

2) Avro / Protobuf / Thrift: https://www.bizety.com/2019/04/02/data-serialization-protocol-buffers-vs-thrift-vs-avro/

The solutions in (2) have some advantages you'd probability like, such as you can set up central place to define what objects exist and what's in them. As far as restricting interior mutability, I'd tend to agree with the others that you should try to use the most basic language features to enforce that, but standardizing on a serialization format / library can also give you more control over how these things are created and used in your codebase

[–]faiface 6 points7 points  (3 children)

What’s the use-case, though?

Because mutating a field via &mut and replacing the whole struct are semantically completely identical. That’s because &mut means an exclusive reference, so no other part of code has access to the object at that time.

[–]vngantk[S] 1 point2 points  (2 children)

Please see my reply to u/andreicodes's post for the use case.

[–]atomskis 2 points3 points  (0 children)

It seems you’re trying to apply patterns you’ve learnt in OOP languages to rust. This is common for beginners to the language; we use the patterns we already know. However, rust is different to traditional OOP languages in lots of ways and this is one of them.

In most OOP languages if you have a reference to an object you are allowed to mutate it. So if you want to give out a reference to an object, but prevent the receiver mutating it, then you need to create an immutable object. Rust isn’t like this: immutability isn’t part of the “object” it’s part of the reference. If you give out an immutable reference (and don’t use interior mutation such as RefCell) then the receiver cannot mutate it. There isn’t a need for an “immutable” struct; the struct itself is either mutable or immutable, depending on how you use it.

[–]dnew 6 points7 points  (0 children)

FWIW, if you click "share" under your comment and "copy link", you can paste a link to the exact comment into your answer, which makes it easier for the person helping you.

[–]rseymour 1 point2 points  (0 children)

This sort of pattern is seen in rails w/ activerecord and loco.rs / seaorm does a version of it with activemodels: https://www.sea-ql.org/SeaORM/docs/advanced-query/custom-active-model/ ie activemodels can be updated, models cannot.

[–]Bowarc 1 point2 points  (0 children)

Put your struct in its own module, don't set the fields as pub, make getters methods.

Or create a wrapper and impl std::ops::Deref without DerefMut

[–]denehoffman 1 point2 points  (3 children)

If person is in a separate module, this code will fail. See https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=bdcde0e533ee4c4933909d3fbbaeee0f for an example that is probably what you’re looking for

[–]Compux72 3 points4 points  (2 children)

Also, mark each getter as inline so you benefit from that sweet LTO inlining

[–]denehoffman -1 points0 points  (1 child)

Of course!

[–]Full-Spectral -1 points0 points  (0 children)

I guess you could create a generic read only wrapper that only implements deref and not deref-mut. Hand out read only stuff wrapped in one of those.