1. I write my own Vec3 class, not fun.
2. I write my own serialization library, could be fun depending on who you are but surely a waste of time.
3. I make a newtype, implement Serializable on it, find-and-replace all Vec3s with my own SerializableVec3, fix about 500 errors one by one by adding .0 everywhere, implement wrappers for all the Vec3 methods I want to use, rinse and repeat for every other foreign type I want to serialize.
4. Hope the math library and the serialization library know each other and the math library implements all the traits you need. In practice this is what happens, but this makes a weird situation where serde is the de facto standard/monopoly because it is literally impossible to use anything else. Which may be fine for now, serde is the standard precisely because it's so extensible, but if you ever want to use another library with traits, well you're SOL and it's back to step 3.
How they managed to ship a language with dev ergonomics this bad is beyond me. Especially when the solution is so simple. Allow foreign traits on foreign types for executable projects only. Put it behind a compiler flag for all I care. -fallow-foreign-traits-I-dont-care-if-this-breaks-things. Just let me implement my damn traits, the current state is ridiculous.
All that aside,
> Especially when the solution is so simple.
This is completely unwarranted and presupposes incompetency or apathy on the Rust dev team, either of which couldn’t be farther from the truth.
Designing programming languages is hard and full of tradeoffs. While we can of course disagree about those tradeoffs, “simple” changes like these are never actually simple in practice and involve complex sets of tradeoffs that invariably have been discussed to obscene lengths. Even “simple” workarounds often commit to implementation guarantees that language designers are hesitant to make until they’re certain it won’t box them into a corner in the future.
Put bluntly, anyone who asserts that a programming language design change is “simple” is only highlighting their own ignorance on the subject.
You can implement that for your types. Deepsize crate implements the trait for a few popular libraries.
You can derive the trait in your structures and it will work for the few structures that happen to contain only your structures and the few hand picked structures supported by the deepsize crate.
And then? What about the 99% of the other cases? Adding a trait impl is acceptable. Wrapping all the other types not much
I've discussed this a number of times with folks on the Rust community Discord. There are unfortunately obstacles to making this happen. One obstacle is that if you implement a trait for a type, then that implementation applies to it everywhere, including in all other library code using the type. By implementing a trait you change its behavior in surprising and likely conflicting ways. Additionally, if you were to implement `Serialize` for some struct, then that would also directly conflict with the struct owner trying to do the same thing.
My proposed solution to this problem is to conceptualized trait implementations as something that can be `use`d. The basic idea is that, if I implement `Serialize` for `Foo` (and `Foo` comes from another namespace) then the type-trait implementation remains private to mine (or perhaps, private to my namespace). That's not how Rust works today, but I'd be curious whether it could work.
I realize this will create a new set of challenges. It will mean `Foo` comes with one set of behavior everywhere else, and another set of behavior in my namespaces where the trait is implemented for it. I don't know enough about Rust to foresee what kind of problems this would cause, but it seems tempting to explore. Usually the kinds of traits that you'd want to implement are not ones that will cause problems for other code.
Personally, I’d like Rust to have “selectable” implementations, which are named and must be explicitly imported, and explicitly “applied” if there are multiple in scope. Selectable implementations always override regular implementations, so you can also replace the library’s provided implementation if it’s buggy or not what you wanted. I don’t even think there’s an RFC for this though…
Aside: newtype isn’t even Rust-specific. It goes to show how popular Rust is and how much Rustaceons love type safety, that when you search “newtype pattern”, the first results are all Rust. The keyword “newtype” comes from Haskell, which also has the orphan rule and associated issues, but at least lets you disable it with a GHC rule. And a zero-cost wrapper is something you can do in Swift, Kotlin, C++, and even C, and the general newtype pattern (although not necessarily zero-cost) is something you can do in practically any typed language, even untyped ones like JavaScript if you consider runtime exceptions ok (use a struct with the custom type name as the field name).
You could also implement Deref (and, if appropriate, DerefMut) on the newtype, though there seems to be controversy over this pattern; for the specific case of a newtype that exists for the specific purpose of enabling foreign trait implementation, it seems to be a fairly straightforward and effective way of dealing with things.
https://rust-unofficial.github.io/patterns/anti_patterns/der...
5. Write a function `serialize_vec3(vec: Vec3) -> String`, and call it in the places you want to serialize a Vec3. The body of the function can convert it to a new type that #derives Serializable, or it can implement the Serializable trait directly.
> one of the worst development experiences I've ever seen in a language
is pure hyperbole. All languages have issues at least as big as this, and most have far far bigger issues.
Name a language and I'll tell you a much worse issue.
Not sure if the offer was only open to OP but I'll bite. How about Java?
If I had to add 500 instances of ".0" to my code, I'd ask myself if I'm taking the right approach to the problem I'm solving. .0 basically means "I don't care about the abstraction the newtype provides, I need the thing inside of it." It will be necessary from time to time, sure, but 500 times? Maybe instead, I can make something like
struct Wrapper<'a>(&'a Vec3<_>);
Then, I only put a Vec3 inside the Wrapper when I actually need to go serialize something."Allowing foreign traits on foreign types for executable projects only" relies on the assumption that every Rust file is either part of a library, or part of an executable. Never both. This assumption is already false because hybrid crates exist, but it's particularly faulty when you consider that the dichotomy of executables and libraries can be extremely blurry in some domains, like in the case of loadable kernel modules or embedded firmware. A systems language cannot make validity choices based on assumptions about underlying ABI/binary formats without kneecapping the language's usefulness.
The monopoly in serialization was inevitable regardless of their design choices about trait coherence. It's far more sane for everyone to agree on a single implementation rather than have 5 different feature flags so everyone can choose their favorite serialization lib. Imagine if you imported a library and discovered that it includes a bunch of functions that return johns_cool_library::Vec instead of std::Vec. Do we complain about std having a monopoly on vectors and strings? No.
The real solution is to open a PR on your math library and add the derives yourself, or fork it. Which is coincidentally what you would have to do in almost any other language, since you can't usually derive an interface for a class when you don't own the interface and the class.
That would mean that the locations where I want to serialize the foreign type would have to be marked with `.into()`, but that would be the sole maintenance overhead.
I do think in practice using `u64` instead of `usize` is meaningless, since there are so few 32-but systems today. The nice thing with the newtype pattern though is that if for whatever reason your program is going to run on a 32-bit instance and you need 64-bit IDs, it’s a 1-line change.
Some of the most widely used MCUs for embedded Rust are 32-bit. But typically you want to be explicit about your sizes when doing embedded anyways.
I think using usize is mostly a convenience that lets you fit values into standard library functions.
This way I don't need to heap allocate objects to ensure their uniqueness. I can still modify them in place without worrying there will be any out-of-date copies that weren't updated. Or I can give out simple ints or even zero-sized structs that can be handles/tokens that grant exclusive or single-use access.
I'd kill for a proper copy constructor to cover the edge cases.
Rust will move structs to a new address if it needs to, but still enforce that there's semantically a single instance (its old address becomes inaccessible).
There's also the type state pattern where you have a struct take a generic so you can allow certain methods depending on the generic type. Like say you have a struct with a field that you want to be optional, but you know statically that at a certain point in the code it'll be set.
You can do:
struct User<IdType> {
id: IdType
}
impl User<()> {
fn create() -> User<u64> {
...
}
}
impl User<u64> {
fn id() -> u64 {
user.id
}
}
That way you can have some methods where you can be certain that the ID is set and some methods where it's not set.