feat!: proper heterogeneous lists - #46
Conversation
5865958 to
5ee0139
Compare
5ee0139 to
847f503
Compare
|
serialization is broken right now |
|
To be honest, I'm not happy with how this solution unnecessarily packages the list content.
What would be nicer, if we can figure out a good way to type it, would be a simple Vec and some way so that, at the same time, NbtTag::NbtList wouldn't require a generic type parameter. |
|
i'm not too sure what you mean by requiring re-homonegisation, considering there is no public API to remove or get a mutable reference to the inner elements. there is also the option of only wrapping things during ser/de. |
|
That is more or less my point. With this implementation we can never get a mutable reference out of an NbtList (without unpacking the internal vec and enforcing the invariant yourself [that invariant being every element having the same type]). I've come up with an alternative design that structurally enforces homogeneity. I'll post it in #45 |
|
removed the type, now it just happens at ser/de time |
|
I have to give this a more thorough look (when I will have a bit of time) before giving the approve stamp, but generally I agree with Fisch37 and I think I like this much more then the previous version. |
|
As I've been asked to give an example that explains what I mean by reproducibility across save-load boundaries, I'll do that with the new behaviour and, after that, provide an outline for what I think would be the best fix for heterogeneous lists. Suppose a program like this: let mut known_players: Vec<NbtTag> = Vec::new();
// Players with a known name are stored with said name
known_players.push(NbtTag::String("Steve".to_string()));
known_players.push(NbtTag::String("Alex".to_string()));
// Players without a known name are stored as their UUID
known_players.push(NbtTag::IntArray(vec![1234,5678, 910, 1112])
// Everything is fine so far
println!("{}", known_players[0]); // "Steve"
// Serializing the data into NBT.
let raw_nbt = NbtTag::List(known_players).serialize();
// ...
// some code around here, probably saves the NBT to the disc or something.
// ...
// some time later, probably having loaded the NBT from the disc. Perhaps this occurs on program start?
let mut known_players = NbtTag::deserialize(raw_nbt).unwrap()
.get_list().unwrap();
// Now the data we've saved comes out differently than it came in.
println!("{}", known_players[0]); // {"":"Steve"}
// If the programmer has not considered this, they will do the same thing they did before
known_players.push(NbtTag::String("RandomUsername"));
// Which means that now known_players is heterogenous again.
println!("{known_players}"); // [{"": "Steve"}, {"": "Alex"}, {"": [I;1234, 5678, 910, 1112]}, "RandomUsername"]
// it gets even worse (though this is technically fixable)
let raw_nbt = NbtTag::List(known_players).serialize();
let known_players = NbtTag::deserialize(raw_nbt).unwrap()
.get_list().unwrap();
// And now everything is wrapped twice!
println!("{known_players}") // [{"": {"": "Steve"}, {"": {"": "Alex"}}, {"": {"": [I;1234,5678,910,1112]}}, {"": "RandomUsername"}]as you can see this approach exposes users to a very-difficult-to-trace bug that they could only have avoided by knowing exactly how this library serializes heterogeneous lists. Even with a documentation, I find this intolerable. My counterproposalI have come up with an alternative approach that,
What I mean is enums, traits, and trait objects. See this mock implementation: /// Container enum for all possible list types
enum NbtList {
Byte(Vec<i8>),
Short(Vec<i8>),
// ...
IntArray(Vec<Vec<i8>>),
// ...
List(Vec<NbtList>),
Compound(Vec<NbtCompound>)
}
impl NbtList {
pub fn get_byte_list(&self) -> Option<&Vec<i8>> {
unimplemented!()
}
pub fn get_byte_list_mut(&mut self) -> Option<&mut Vec<i8>> {
unimplemented!()
}
}
impl Index<usize> for NbtList {
type Output = dyn NbtCompatible;
// ...
}
impl IndexMut<usize> for NbtList {
// ...
}
/// This trait indicates that a type can be contained in an NbtTag or NbtList.
trait NbtCompatible {
// Wraps this NbtCompatible in its corresponding NbtTag variant.
fn wrap(self) -> NbtTag;
fn get_type_id(&self) -> u8;
}
// implemented for any value that can be contained in NbtTag as well as `Vec<T> where T: NbtCompatible`
impl NbtCompatible for i8 {
fn get_type_id(&self) -> u8 {
unimplemented!()
}
}
impl dyn NbtCompatible {
fn get_byte(&self) -> Option<&i8> { }
fn get_byte_mut(&mut self) -> Option<&mut i8> { }
// etc etc
}Pros
Cons
ToDos
I'll open up a PR on Vonr's fork once I have made a full implementation. |
|
the given example no longer happens with the new code, wrapper compounds are unwrapped during deserialization. the point about memory usage is fair though |
this change is API breaking.
fixes #45