7 ms·
Rust Design Patterns as a Book
- ncmncm 6y agoA list of design patterns for a language amounts to a list of weaknesses in the language or in its library. If the pattern could be captured in a library, that pattern would just be using the library. If the core language provided the feature, the pattern would just amount to using the feature. Generally it is better to improve the language to the point where the feature can be added as a library component, but sometimes that is too hard. Thus, for most languages nowadays a "dictionary" type is provided in the core language, because a useful hash table library cannot be written with language primitives. In C, hash tables are open-coded again in each place where one is needed, because the language provides neither the feature, nor facilities sufficient to capture it in a library. Rust is powerful and expressive enough that hash tables are library components. Conversely, pattern matching and coroutines are built into Rust. It should never be forgotten that (1) this was because the language was not expressive enough to capture the features satisfactorily in a library; and that (2) it would be better if, someday, the core feature became unnecessary because the language became expressive enough provide it as a library. One reason it is better for features to be provided in a library is that another library can implement a variation on the feature that might not be as widely useful as the core version, but is better tuned to a less-common but still important use. Another is that users can invent whole new features by combining powerful primitives that the language designers would not have time, or possibly inclination, to implement themselves. Thus, in a certain sense, all patterns are anti-patterns.
- ldiracdelta 6y agoI'm a bad man, and I have sinful [ooo] thoughts, but... Is-a inheritance is extremely useful for creating extensible components. "It may be wrong, but it's much too strong." In rust, how can you make a component that is just like another component, but ever so slightly tweaked without copying the entire external API of that other component? I understand I can wrapper with has-a relationship, intercept the correct API, and then pass through the rest of the entire interface, but how can I avoid copying the entire interface of the object when I only want to tweak something tiny? With a car, I can swap out the engine with another, I just have to make sure the external interface is the same. It may be a "bad thing", but it is extremely useful for the scenario where I say, "I want a Chevy smallblock, but I want to only tweak metal alloy on the interior piston." class MyBlock(ChevySmallBlock): def get_interior_alloy(self): return metals.Unobtanium Bam. I have same item; slightly tweaked. I've used this type of pattern to great effect and I find that style of inheritance manipulation invaluable in python. How can you do this with rust? I know that is-a inheritance is sinful, but show me the better way! I truly want to know it, and I've been trying to find a pattern for this.
- techsin101 6y agokinda related topic: im debating if I should learn Rust. I'm attracted to Go, but at the end of the day I can do everything that Go can with Nodejs minus the speed part. Rust has just way to many new concepts to simply learn it. Why did you learn Rust over Go. How are you using it?
- Twisol 6y agoIn section 2.10, "Privacy for extensibility", are there any pros and cons to this approach over using the #[non_exhaustive] attribute? The latter works on both enums and structs, and doesn't require extra fields to be included. https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute https://doc.rust-lang.org/reference/attributes/type_system.h...
- varajelle 6y agoIt looks like the book is outdated in this respect
- michalhosna 6y agoUsing private fields you can more precisely control the “private scope”. #[non_exhaustive] is “crate scoped”, it does not apply limits for use in the same crate. Private fields are by default module scoped, and can be tweaked. So you can limit the use even in the same crate.
- codeflo 6y agoA very low-effort way to learn good Rust patterns is to put #![warn(clippy::all)] at the top of your crate’s entrypoint. This enables Rust’s default linter. It’s a lot more friendly and focused on good design than you might expect, often suggesting more elegant alternatives. Plus, many of its suggestions can be applied automatically in an environment like VS Code + rust-analyzer plugin.
- tasn 6y agoThanks a lot for the suggestion, I can't believe I didn't know about this. However I just tried it and I can't get it to work. I added this to the top of https://github.com/etesync/etebase-rs/blob/master/src/lib.rs https://github.com/etesync/etebase-rs/blob/master/src/lib.rs and then ran `cargo clippy` #![warn(clippy::all)] // Should fail https://rust-lang.github.io/rust-clippy/master/index.html#float_cmp pub fn bool_test(x: f32, y: f32) -> bool { x == y } Any idea what's missing? Why is it not failing? Edit: I know the above example is bad code, that's the point. I want clippy to complain about it but it doesn't.
- smnscu 6y agoFloat comparison is an anti-pattern. Use an epsilon instead. https://stackoverflow.com/questions/4915462/how-should-i-do-floating-point-comparison https://stackoverflow.com/questions/4915462/how-should-i-do-...
- tasn 6y agoI know, it's an example I was hoping clippy would catch in order for it to fail so I know it works. Read what I wrote...
- conradludgate 6y agoComparing floats by equality is a dangerous pattern. It's easy for small precision errors to occur. You should instead check that they are close enough to each other, using an epsilon that you find appropriate, perhaps 1e-10. `(x - y).abs() < epsilon` should do the trick
- staticassertion 6y agoHaving documents like this is awesome - thanks for building it. Is there somewhere I can PR? One example - the `new` constructor takes no arguments, so at minimum there should be a note about the (mentioned-next) Default pattern. Also, I don't think Default is most useful for abstracting construction (I think closures are better for this), they're really just to make construction easier imo ie: Foo { prop: override_default, ...Default::default(), } edit: It's hosted on github, duh, nvm
- vbrandl 6y agoIs there anything that can be achieved by using the visitor pattern [0], that cannot be done by using pattern matching? I have only used the visitor pattern in languages that do not have pattern matching as a language feature (e.g. Java before it got a Scala-like `switch` construct [1]). Edit: one limitation of pattern matching is, that all values need a common supertype (e.g. be variants of the same enum in Rust, if we see each variant as a type and the enum as the common supertype. There is an RFC [2] to make enum variants accessible as types), while the visitor pattern could be implemented for any set of independent types. On the other hand, you then cannot have a typed collection/container that contains values of these types, so you'd need some common trait like `Visitable` so you could accept an `Vec<dyn Visitable>`. [0]: https://rust-unofficial.github.io/patterns/patterns/visitor.html https://rust-unofficial.github.io/patterns/patterns/visitor.... [1]: https://openjdk.java.net/jeps/8213076 https://openjdk.java.net/jeps/8213076 [2]: https://github.com/rust-lang/rfcs/pull/2593 https://github.com/rust-lang/rfcs/pull/2593
- MetaDark 6y agoThe Visitor pattern is great if you want to process a data structure as "stream" without actually instantiating it, in the same way you can read a file line-by-line instead of loading it completely into memory. For example, serde uses the visitor pattern to encode its intermediate representation. If it used pattern matching instead of the visitor pattern, it would have to instantiate its intermediate representation as an enum, which would add unnecessary overhead.
- vbrandl 6y agoI guess you are talking about this `Visitor` trait [0]. I have only used serde in combination with the derive macros so please enlighten me, but each `visit_* ` function of the `Visitor` trait already takes a typed value (`bool`, integer types, ...) so these already have too be parsed (instantiated?) from the input string/bytes. Couldn't you have an `SerdeTypes` enum that implements `From` for each `visit_* ` type to remove some boilerplate and then use pattern matching? Could you elaborate where there would be overhead? Don't get me wrong. I'm sure, the serde developers know better than me and have good reasons to implement it the way they did, but I'd like to understand the rational behind the decision. Edit: formatting [0]: https://github.com/serde-rs/serde/blob/master/serde/src/de/mod.rs#L1263 https://github.com/serde-rs/serde/blob/master/serde/src/de/m...
- ritchiea 6y agoOnce you get past writing a language idiomatically, is a list of design patterns a good thing? It is an obvious negative for code readability because it reduces the number of people who can clearly understand your code from Rust users to Rust users who also memorize design patterns. When are design patterns useful? And how are they useful?
- bonzini 6y agoDesign patterns are not necessarily less understandable, much the contrary in fact. They basically encode known good ways to do something, so it should be okay even for beginners who haven't learnt the patterns yet.
- ritchiea 6y agoWhat's more or less understandable is a bit subjective. There are some things that are obviously less complex than others. Some things that are obviously more complex than others. And then a lot of gray area. In my experience there is at least a faction of developers, myself among them that have a disdain for "design patterns thinking." Which I would describe as: spending a lot of focus learning various design patterns, then while coding actively looking for places where those patterns could be put to use. In my opinion this is an anti-pattern similar to overuse of abstraction in simple cases before an abstraction adds to the understanding of the code itself, rather than makes the code more complex. I've seen lists of common design patterns dozens of times, and occasionally recognize several of them as useful examples of things I've actually done in the past or my colleagues have done in the past. But it seems to me "learning design patterns" as an end is encouraging the destructive side of design patterns where you learn something and eagerly look for a use for it.
- paledot 6y agoIs that really any different from leaning a language, tool, framework, library, service, etc.? You learned the new thing, you're excited to try to out, and you should still be judicious in analyzing whether this is or isn't the place for it. And some developers just want to play with the new shiny. None of that is an argument against learning new things. Just keep adding tools to your toolbox, so you don't end up with every problem looking like a nail.
- boomer918 6y agoSome idiomatic suggestions seemed to replace lacking language features, which is a smell to me; I think it's best to use the language as designed instead of creating something weird and maybe even unstable like a "finally" block using the Drop trait for a dummy struct. Design patterns were weird too. The builder pattern to hide complex initialization? Not a fan; maybe it would be best to remove the complexity instead? Rust is a functional language, so OOP patterns seem like an anti-pattern.
- brundolf 6y agoWait what? You can use dyn on the stack (without a Box)? This has been one of my biggest complaints about Rust: I've been using it for years at this point. I read most of the Book, I've read a few unofficial books. And I do love the language, but it has so many cases like this where things that you're allowed to do (syntactically or otherwise) are somehow so non-obvious that you can miss them entirely. I still get blindsided by something like this every couple months, and I always end up a little mad that I've been doing things the hard way until some obscure unofficial material (or more often, a stack overflow answer) teaches me about an entire feature that I didn't know existed. Rust is really great at telling you what you can't do, and in many ways its documentation is incredibly thorough, but it has a real problem when it comes to discoverability and establishing a consistent mental-model of what its syntax actually means (and how you can then apply it to other situations). I don't know what the root cause of this problem is. But it's really distressing to me each time I discover a huge blind-spot; it makes me feel like I never fully understood the language concepts that I thought I understood. I say all of this out of love: I really want Rust to succeed. I still prefer to use it despite this issue. I just believe this is a huge thorn in its side, especially when it comes to adoption, for which it already has an uphill climb.
- steveklabnik 6y agodyn just requires the object to be behind some kind of pointer. The vast majority of the time, that pointer is a Box or an Rc/Arc, but any form of indirection can work.
- brundolf 6y agoTaking a second look at this particular example, I guess it is a bit of a "trick" because of the ahead-declaration of both possible "holder" variables on the stack. It's just... wildly unintuitive that this should be possible. Even if you showed me this code without saying whether or not it should compile, I wouldn't be sure. Here's the train of intuition: 1) dyn requires a pointer that may be to one of multiple types of structs 2) a group of multiple types of structs has an undefined memory layout, so the value must either live on the heap or be wrapped up in an enum That feels like an airtight understanding. But then Rust lets you do this weird juggling maneuver based on control-flow that allows you to do it on the stack. I'm not saying Rust shouldn't let you do this, and I'm not really sure how it could be made intuitive given the "normal" case. I'm just expressing that subjectively, this feels very weird and non-obvious, and it's far from the first example like this that I've encountered. Here's another example: https://news.ycombinator.com/item?id=25595120 https://news.ycombinator.com/item?id=25595120
- acje 6y agoPatterns are nice, but dangerous to follow blindly in an immature and fast growing industry operating like a pop culture. As an example the book suggest to use YAGNI, a pattern/principle Jim Coplien suggest we fight. I’m inclined to agree. Plan ahead when you can. And btw pull in decisions to get early feedback in stead of delaying to the last responsible moment. Just some thoughts, but I like that patterns are collected for different contexts. It allows for discussion and learning.