[HN Gopher] Patterns with Rust Types
___________________________________________________________________
Patterns with Rust Types
Author : mre
Score : 79 points
Date : 2023-09-01 13:16 UTC (9 hours ago)
(HTM) web link (www.shuttle.rs)
(TXT) w3m dump (www.shuttle.rs)
| heinrich5991 wrote:
| As was already mentioned in the Reddit thread on /r/rust (https:/
| /old.reddit.com/r/rust/comments/wb10zi/patterns_with_...): It's
| weird to take a platform-dependent integer for a database ID. Use
| `u32` or `u64` instead of `usize`.
| armchairhacker wrote:
| I find `usize` in Rust is like the default unsigned integer,
| used for nearly everything and especially everything index-
| related.
|
| 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.
| heinrich5991 wrote:
| It was explicitly renamed away from `uint` to make it clear
| that it's not the default integer. Rust tries to encourage
| using explicit bit widths.
| sshine wrote:
| > _there are so few 32-bit systems today_
|
| 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.
| hardwaregeek wrote:
| Builder patterns are also super helpful for libraries. Allows you
| to create APIs with lots of optional fields without having call
| sites that pass in a dozen None values.
|
| 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.
| nmilo wrote:
| The no-foreign-traits-on-foreign-types rule and the terrible
| ergonomics behind the newtype "pattern" combine to make one of
| the worst development experiences I've ever seen in a language.
| It basically means you can only use one large library, ever.
| (With caveats.) For example, if I'm writing a game, I include a
| math library and pull in a Vec3 type, then add Vec3 as a member
| to all my objects, and then later I want to pull in a
| serialization library that defines, for ex, a Serializable trait,
| I can't, unless:
|
| 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.
| fiedzia wrote:
| 3. should be one click in your IDE.
| rdedev wrote:
| Would phantom types help in this case ? I know that rust
| supports it
| staunton wrote:
| I strongly disagree that these are big problems in practice.
| There are macros you can use to make newtypes more ergonomic to
| use and reduce boilerplate, etc. The advantage it gives is
| forced adherence to many implicit assumptions of library
| authors, which means that it's a lot easier (read: preventing
| subtle bugs) to compose different libraries. Meanwhile,
| allowing it might split and completely break the ecosystem.
| pdimitar wrote:
| Can you give examples of those macros? Very interested in
| using them.
| duped wrote:
| Most languages don't even have the anatomy of features that
| lead to this problem in the first place.
|
| 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.
| jcrites wrote:
| I agree with you. I'd like to see Rust change to allow defining
| traits on foreign types, somehow.
|
| 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.
| ithkuil wrote:
| What if the author of the trait explicitly marks it to allow
| that?
| armchairhacker wrote:
| This isn't an issue with the "newtype" pattern, it's an issue
| with orphan instances. Newtypes have other uses (e.g.
| contracts, type-safety for integers and other data which has
| specific meaning). This is just using the newtype pattern to
| get around the fundamental issue, which is that you can't
| define a third-party instance for a third-party type.
|
| 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).
| justinpombrio wrote:
| You're missing:
|
| 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.
| dragonwriter wrote:
| > 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.
|
| 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.
| rascul wrote:
| That pattern is documented here
|
| https://rust-
| unofficial.github.io/patterns/anti_patterns/der...
| IshKebab wrote:
| It's definitely annoying. But to say it's
|
| > 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.
| nathanwh wrote:
| > 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?
| IshKebab wrote:
| I haven't used Java for some time, but when I did it didn't
| have non-nullable types. I think collections were also type
| erased which is pretty bad.
|
| JNI is a nightmare.
|
| Strings are UTF-16.
|
| Dealing with installing the JVM, whatever the hell
| "classpath" is, tweaking GC parameters etc. is a big
| downside.
|
| Despite that it's definitely one of the saner languages out
| there.
| pkolaczk wrote:
| 1. Java does not allow to implement new interfaces for
| classes without modifying their signature - much more
| limiting than Rusts foreign trait rule. You cannot make a
| foreign class implement your new interface.
|
| 2. Java does not allow to create wrappers without incurring
| a significant memory overhead. A wrapper will consume at
| least 32 bytes of memory on 64-bit systems just for itself,
| when in Rust this can be 0.
|
| 3. Even if you define a wrapper, it would be way less
| ergonomic than in Rust - there is nothing like AsRef /
| Deref / From / Into etc machinery in Java.
| monocasa wrote:
| Boxed Integers will compare with == for values -128 to 127
| but not other valid ints.
| Integer.valueOf(5) == Integer.valueOf(5) true
| Integer.valueOf(200) == Integer.valueOf(200) false
| chaosite wrote:
| That's a quirk of the language, sure, but it's barely an
| issue, and definitely not a worse issue. Don't get many
| wrong, Java has many issues, but this barely qualifies.
|
| You don't compare non-primitives (which boxed integers
| are) with == in Java, you use the equals method.
| earthling8118 wrote:
| You definitely shouldn't compare them that way. But it
| still allows you to. That becomes a fairly big issue
| where you have the behavior working when you test it out
| and suddenly it doesn't work when the values become
| larger. Sure, an experienced Java developer will know
| this. How about a developer that is new to the language?
| Not as likely. I've personally lost over 6 hours to that
| one years and years ago.
| monocasa wrote:
| You're not supposed to, but I've certainly found code out
| there that does use == to compare boxed integers because
| the test cases they used happen to work.
|
| And in fact when I googled to find the exact range where
| it switches from cached integers to truly creating new
| objects, the top link was at best highly misleading.
| https://www.tutorialspoint.com/check-two-numbers-for-
| equalit...
| paulddraper wrote:
| .valueOf() will sometimes give different instance,
| sometimes not.
|
| If you want a instance, I recommend new :)
| monocasa wrote:
| Well, unfortunately autoboxong will silently call
| valueOf(). So Integer val1 = 5;
| Integer val2 = 5; Integer val3 = 200;
| Integer val4 = 200;
|
| will have you ending up with val1 and val2 being equal
| with ==, but val3 and val4 won't be.
|
| The general reply is 'you're holding it wrong', but that
| doesn't make it any less absurd. That's what the PHPers
| say about "123" < "456A" < "78" < "123".
| nmilo wrote:
| Okay, I was being a bit hyperbolic and I'd rather not go back
| and forth, but I do really hate the newtype pattern. I'm not
| sure why they don't introduce something like what Go has,
| like `type UserID = uint32`.
| jcrites wrote:
| Rust has that, it's called a type alias: https://doc.rust-
| lang.org/reference/items/type-aliases.html
|
| I'm not familiar with that Go feature, but in Rust it's
| just introducing another name for a type. I've usually seen
| it used to introduce simpler names for complex types.
| duped wrote:
| Type aliases and new types are two subtly different
| features. The new type pattern allows you to provide new
| impls for the underlying type (it is a new type after
| all) while the type alias is pure sugar where type T = U
| allows the string T to be substituted for U in any place
| where T is imported. If U is a foreign type you cannot
| provide new trait impls for it.
| [deleted]
| ryukoposting wrote:
| The effort involved in (3) can be substantially reduced with
| some AsRef/AsMutRef/Deref/etc implementations on the wrapper
| type.
|
| 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.
| MrBuddyCasino wrote:
| The majority of Rust code isn't kernel extensions or embedded
| firmware, and its not like those use cases suffer any
| downsides by allowing the majority some more leeway.
| Therefore this strikes me as perfect being the enemy of good,
| but I guess that is Rust's culture.
| stouset wrote:
| I won't deny that the newtype pattern leaves a lot to be
| desired, but I think I'm practice this isn't nearly as big a
| deal as you make it out to be.
|
| 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.
| ithkuil wrote:
| Another example: https://crates.io/crates/deepsize
|
| 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
| nmilo wrote:
| I didn't mean any offence when I called the change simple,
| though I do now see how it can be interpreted that way. When
| I call it simple it's because I _am_ ignorant about
| programming language design changes, because, well, I don 't
| really care. I'm not a PL designer. The change is simple to
| me because it: (a) seems arbitrary and easily changed, (b)
| solves a class of problem that I've never run into in other
| languages nor can ever really envision happening in a
| realistic scenario, and (c) if there are complex sets of
| tradeoffs involved in enabling this feature then they haven't
| been communicated well to the end-user at all. Especially
| with how bad the current state of affairs is, my calling the
| change simple should be a sign to the Rust team that if this
| really is the best possible decision for the language, they
| should put a clear statement in the compiler saying why.
| Because as it stands, from my end-user perspective, I still
| haven't seen a great reason why this shouldn't be
| implemented.
| [deleted]
| [deleted]
| cowthulhu wrote:
| Does anyone know of any sites that list out articles like this by
| language? I always find it tough to locate more advanced or
| "meta" articles like this that aren't just explaining basic
| concepts.
| pornel wrote:
| I love that structs can be strictly non-copyable.
|
| 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.
| monocasa wrote:
| Unfortunately I haven't been able to find a way to make structs
| completely noncopyable except via Pin. The compiler is
| reference tracking, and if it's sure that there aren't
| references it'll copy the value around. So for instance full
| RVO isn't guaranteed, and you can catch the bit pattern of
| &self changing out from under you even in a struct that doesn't
| implement Copy.
|
| I'd kill for a proper copy constructor to cover the edge cases.
| pornel wrote:
| You're expecting a _stable address_ , but that's a different
| thing.
|
| 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).
___________________________________________________________________
(page generated 2023-09-01 23:02 UTC)