this post was submitted on 11 Jul 2023
9 points (100.0% liked)

Rust

5965 readers
16 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

[email protected]

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 1 year ago
MODERATORS
 

I'm wondering with my game project how best to handle types. I'm basically fetching all the rows in a particular table and creating a struct to represent that data. I then have like 5 or 6 sequential steps I'm trying to go through where I need to modify those initial values from the db.

My first thought was to have a base type called 'BaseArmy', then I needed to add new temporary properties that are not in the db and not represented in the original struct, so I decided to create a new struct 'Army'. The problem is I keep running into errors when passing converting from BaseArmy to Army. I tried writing a From impl, but it wasn't working (and I'm not even sure if it's the approach I should be taking).

So should I:

  1. Have multiple different types to handle these kinds of cases
  2. Have just one type somehow where I add properties to it? If so, how? I recently tried using Options for the fields that are not initially available, and that seems to be working but it feels weird.
top 8 comments
sorted by: hot top controversial new old
[–] [email protected] 5 points 1 year ago (1 children)

What errors are you getting when converting from BaseArmy to Army? If you're storing references to other structs in your struct, things can get complicated. Using Clone/Copy types will make things easier.

I'd also recommend the builder pattern for this. It let's you build up a type incrementally, with a final build step that produces the final type.

[–] [email protected] 1 points 1 year ago* (last edited 1 year ago) (1 children)

It was basically me passing BaseArmy in as a param to a fucntion, then returning an Army type. I tried a few different things, but what I really wanted to do was just spread out the struct like I would in Typescript. Rust seems to support this UNLESS there's one field that's different.

Let me give builder pattern a try. I was literally just learning more about it, but didn't think to apply it here.

EDIT:

Here's what' I'm trying to do: Battalion { count: count, position, ..db_battalion_template }

Then the error I get:

mismatched types expected Battalion, found BattalionTemplate

EDIT2:

After more fiddling around and adding the from conversion:

Battalion { count: count, position, ..Battalion::from(db_battalion_template) } impl From<BattalionTemplate> for Battalion { fn from(a: BattalionTemplate) -> Self { let serialized = serde_json::to_string(&a).unwrap(); Self { position: 0, ..serde_json::from_str(&serialized).unwrap() } } }

I get this error: thread 'main' panicked at 'called Result::unwrap() on an Err value: Error("missing field position", line: 1, column: 227)'

Edit 3:

I at least got the from conversion to work by just manually specifying the Battalion fields. Should I just accept that I can't spread struct properties from one type on to another unless they have exactly the same fields?

[–] [email protected] 2 points 1 year ago

Yeah, you can't pass one type when the function expects another type. You have either use generics and trait bounds or provide the exact type that a function expects..

[–] [email protected] 3 points 1 year ago

I don't know how you're trying to do this BaseArmy / Army conversion. Perhaps you should try and show this code?

[–] [email protected] 2 points 1 year ago* (last edited 1 year ago) (1 children)

Rust has a strong emphasis on strongly typing constraints. So if a collection of fields are always some/none together having them listed as separate options in the struct means there is some assumptions you are making that the type system isn't aware of which can lead to pain and bugs in the future.

I agree that separate types with Into sounds like a nice solution to me, it would be good to see the error the compiler is giving you (or if you can a minimal reproducible repo). If you absolutely can't make it work, a single Option<Inner> at least would be more correct as all the fields on the inner struct would be optional together.

[–] [email protected] 2 points 1 year ago* (last edited 1 year ago) (1 children)

If you absolutely can’t make it work, a single Option at least would be more correct as all the fields on the inner struct would be optional together.

Wait really? If I wrap a struct in Option it makes all the fields optional?

Good to know that you think the Into approach seems better. Part of the purpose of this thread is to just gauge what's the better way to do this in Rust. Do you know what "separating the types with Into" would look like?

[–] [email protected] 2 points 1 year ago (1 children)

Yeah, sort of. I probably didn't explain super well, and also probably don't fully understand the problem so here are some code snippets that might make things more concrete and you can tell me where my asumptions of your codebase are wrong

So first off we have what I assume you were suggesting with multiple options for the individual db props. I commented where things are painful and bug prone:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2ecb08dd5d54378c693a7014599e8645

If Option is indeed the aproach you want to take we can solve a lot of these problems by moving all the fields that go optional together into a separate struct:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=9eb2f896e171507acb64283d5b530673

We get even more type safety and clarity by making them separate types (and have ArmyWithDbProps wrap Army):

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c2e33aa1c09538ffdbe0cd440bbff3e1

Or we could use Into if it's not appropriate for process_army_from_db to turn an ArmyWithDbProps into an Army:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f82043935625572cb08c240f23944f0e

Also this last example is using clone when if this is in-fact the direction you want to go we could be using pointers to avoid unnecessary clones. Let me know if this is the case and I can write a version with pointers and lifetimes.

I'm sure we've missed something here specific to your software (obviously the above links are all trivial examples), but I just wanted to help clarify my original point with some concrete code. If you can share some of your code we might be able to give you more specific advice.

[–] [email protected] 2 points 1 year ago

I'd provide more code but it's a mess and full of old commented-out code. Your examples are perfect! combining the DB fields into it's own struct is something I hadn't thought of... and I totally get why having a bunch of options sitting in the Army struct would be problematic. I'm really excited about rust for moving these sorts of errors to compile time.

The INTO example seems great too. I'm ok with the performance hit of cloning for now... lifetimes and pointers feel like a tier above where am at with my rust skills, and I'll circle back to get a better handle on them later.

One question about the INTO example... I always hear it's better to just implement FROM and get INTO for free. Does that not make sense for my use case? If I did it, would it look something like:

impl From<ArmyWithDbProps> for Army { fn from(self) -> ArmyWithDbProps { self.armyWithDbProps } }

load more comments
view more: next ›