Rust requires a mindset shift
That’s easier said than done. I find that there’s no clear vision of what “idiomatic” Rust is. With functional programming, it feels like there’s a strong theoretical basis of how to structure and write code: pure functions everywhere, anything unpure (file access, network access, input conversion, parsing, etc.) goes into a monad. TBF, the only functional code I write is in JS or
nix
and nix-lang is… not really made for programming, nor is there any clear idea of what “good” nix code looks like.Rust however… are
Arc
,Box
,Rc
,async
, etc. fine?match
orif/else
? How should errors be handled? Are macros OK? Yes, theclippy
linter exists, but it can’t/won’t answer those questions for you. Also the fact that there is no inheritance leads to some awkward solutions when there is stuff that is hierarchical or shares attributes (Person -> Employee -> Boss -> … | Animal -> Mammal-Reptile-Insect --> Dog-Snake-Grasshopper). I haven’t found good examples of solutions or guidance on these things.My rust code still feel kludgy, yet safe after a year of using it.
Rust however… are Arc, Box, Rc, async, etc. fine?
Yes, these are all fine to use. When you should use them depends on what you are doing. Each of these has a usecase and tradeoffs associated with them. Rc is for when you need multiple owners to some heap allocated data in a single threaded only context. Arc is the same, but for multithreaded contexts (it is a bit more expensive than an Rc). Box is for single owner of heap allocated data, async is good for concurrent tasks that largely wait on IO of some sort (like webservers)
match or if/else?
Which ever is more readable for your situation. I believe both are equally powerful, but depeding on what you are doing you might find a simple
if let
enough, other times amatch
makes things a lot more succinct and readable. There are also a lot of helper functions on things you often match on - like Result and Option that for specific situations can make a lot more readable code. Just use which ever you find most readable in each situation.How should errors be handled?
This is a large topic. There is some basic advice in chapter 9 of the book though that is mostly about Result vs panic. There are also quite a few guides out there about error handling in rust in a broader sense.
Typically they suggest using the thiserror crate for errors that happen further from main (where you are more likely to care about individual error variants - ie treating a file not found differently from a permission denied error) and the anyhow crate or eyre crate for errors closer to main (when you are dealing with lots of different error types and don’t care as much about the differences as you typically want to deal with them all in the same way when you are near to main).
Also the fact that there is no inheritance leads to some awkward solutions when there is stuff that is hierarchical or shares attributes (Person -> Employee -> Boss -> … | Animal -> Mammal-Reptile-Insect --> Dog-Snake-Grasshopper).
I have rarly seen a system that maps well to an inheritance based structure. Even the ones you give are full of nuances and cross overs that quickly break apart. The classic animal example for instance falls flat on its face so quickly. Like, how do you organise a bird, a reptile, a dog, a fish and a whale? You can put the whale and dog under mammal, but a whale shares a lot of things with the fish, like its ability to swim. Then think about a penguin? It is a bird that cannot fly, so a common fly method on a bird type does not make any sense as not all birds can fly. But a penguin can swim, as can other birds. Then just look at the platypus… a mammal with poison spurs that swims and lays eggs, where do you put that? Where do you draw the lines? Composition is far easier. You can have a Fly, Swim, Walk etc trait that describe behaviour and each animal can combine these traits as and when they need to. You can get everything that can fly in the type signature, even if it is a bird, a bat, or even an insect. Inheritance just cannot do that with the number of dimensions at play in any real world system.
IMO it is simpler and makes more sense to think in terms of what something can do instead of what something inherits from.
I recently caught myself trying to do traits in an OOP language. Failed spectacularly of course.
But it would be so much easier to read…
Depending on your language, your closest analogue is going to be interfaces. C# even has a where clause where you can restrict a generic type such that any type substituted must implements one or more interfaces. You can get quite a bit of trait like working there, from the function input side of stuff.
The biggest problem is, you can’t implement an interface for a type unless you have access to the type. So you’d have to really on wrapping types you don’t own to apply trait like interfaces to it.
And then there’s minor issues like, no such thing as associated types, or being able to specify constants in a definition. But you can usually work around that in a less nice way.
In my case, it was in Dart. Dart allows extending existing classes with new methods, but unfortunately this doesn’t allow implementing abstract mixins (which is the equivalent of Rust’s trait) on other types. Dart is in this weird middle where it’s not really strictly typed (it has
dynamic
, which is like theany
type in TypeScript), but the compiler doesn’t allow ducktyping anyways.
Precisely. I work in Python a lot, and almost all of our classes use mixins because there’s single inheritance rarely is that answer. And that’s basically just an abuse of inheritance and a trait system would probably be a better fit.
people write oo code in linux source code using c. With rust is simple. Just use composition, and you can explore the traits, that serves as and is better than interfaces.
@onlinepersona @snaggen another problem I see people doing while writing rust, is trying to write code like java. Rust isn’t an OO language, but you can organize your code and have hierarchies.
Rust isn’t an OO language, but you can organize your code and have hierarchies.
IMO I think this is a common fallacy. OOP does not mean inheritance/hierarchies (despite them being part of every introductory OOP course nowadays). The original meaning of OOP had nothing to do with inheritance, that idea was mostly popularised by Java. And these days not even Java devs recommend inheritance as the first port of call but instead often favour composition and interfaces as the better language constructs.
Rust is as good at OOP styles as it is functional or procedural, if you ignore inheritance as a requirement of OOP. And a lot of code in rust can look and feel like OOP code in other languages. The abilities to encapsulate state, and polymorphism your code are far better features of OOP and both are well supported in rust. IMO rust offers the useful features from all paradigms fairly equally, which lets you write in any style you like, or even mix and match depending on the various situations. As one is not always better then the others, but each alone is useful in specific situations. More languages should be like this rather than forcing everything into one mold as it lets you pick the best style for each task.
Sometimes I wonder if this pure search for being “idiomatic” is worth the effort. On paper yes, more idiomatic code is almost always a good thing, it feels more natural to create code in a way the language was designed to be used. But it practice, you don’t get any points for being more idiomatic and your code isn’t necessarily going to be safer either (smart pointers are often “good enough”). I’m fine using references to pass parameters to function and I love the idea to “force” the programmer to organize objects in a tree way (funny enough I was already doing that in C++), but I’ll take a Rc rather than a lifetimed reference as a field in a structure any day. That shit becomes unreadable fast!
EDIT: but I love cargo clippy! It tells me what to change to get more idiomatic points. Who knows why an if/then/else is better than a match for two values, but clippy says so, and who am I to question the idiomatic gods?
@Blackthorn @onlinepersona Feels the best when you don’t have to do either.
If you want inheritance you can add structs as parameters of other structs. This will also allow them to use impl functions for that struct.
As far as I understand it Arc<> is just the Async version of RC<>.
I’m not entirely sure about Box<> and a lot of its API’s are still unstable but I believe it’s primarily used as an owner for unsafe things.
As far as I understand it Arc<> is just the Async version of RC<>.
Not quite right.
Arc
is the atomic RC, aka the one that is thread safe and can thus can be sent to other threads. Rc is single threaded only. async is agnostic of threading and there exists runtimes that are both multithreaded and single threaded.Although tokio, the most common async runtime, is multi threaded and so tasks you create need to be multi-threaded safe thus you likely need to use Arc for most things in tokio. But that is due to the multithreaded nature of tokio, not the fact it is async.
I’m not entirely sure about Box<> and a lot of its API’s are still unstable but I believe it’s primarily used as an owner for unsafe things.
Box is the single ownership heap allocated datastucture of rust. It is a core type that is used for a lot of things. It is not just for unsafe things and mostly safe things are put in a Boxes. It is basically used whenever you want something on the heap rather than the stack At least when the type in question is not already heap based (like Strings, Vec, Arcs etc).
It is used a lot for trait objects (when the type is only known at runtime, not compile time, aka
Box<dyn Trait>
/&dyn Trait
) when you need ownership of the object (aka reference trait objects are not suitable). Or when you have a large type that needs to be moved around a lot and you don’t want expensive stack copies when a cheap copy of a pointer to some heap data will do instead. Or you have a type that is not Sized (aka the size is not known at compile time and needs to be tracked at runtime) but need it to be owned (such as a slice, trait objects etc).Ahh, thank you for the corrections.
Box
is (basically) just the way to have memory on the heap. Here’s a direct comparison of how to do heap memory in C/++ and in rust:int* intOnHeap = (int*)malloc(sizeof(int)); *intOnHeap = 0; MyClass* classOnHeap = new MyClass();
let intOnHeap: Box = Box::new(0); let structOnHeap: Box = Box::new(MyStruct::default());
There can be a bit more to it with custom allocators etc. but that’s the only way most people will use boxes. So
Box
basically just means “aT
is allocated somewhere and we need to free that memory when this value is dropped”.
Java devs have some of the worst habits when they learn a new language. Why create a global variable for a setting in python when you can instead wrap that global variable in a Singleton class that makes using it no safer, and far more complicated
Actually, I like encapsulating global state in a structured and documented construct. But I guess I could see Java developers going overboard with abstraction in an imperative language.
I think, the point is that in Python (like Rust) that construct is a module. In Java, a module does not exist in that sense. You put everything into classes in Java and static classes, i.e. singleton objects, are what you use instead of modules…
Interesting, that definitely makes sense!
There are good reasons to do singletons in python. But your first instinct shouldn’t be to jump straight to one
Or do we use an Arc such that our dependent services hold onto an Arc>, allowing concurrent access of the owned resource?
Arc is already heap allocated, there is no need or point in Boxing an Arc. They serve the same purpose except Box is single owner and Arc is multi owner. Box is not the only smart pointer that supports trait objects:
Arc
is also allowed, same goes forRc
and other smart pointer types.Though the answer to that question as it stands is: it depends. Both methods they suggest are valid approaches with different tradeoffs. Another is to just forgo trying to share a single field and share the whole object, ie have
Arc
or&Service
instead. Or clone the whole thing between threads, or many other patterns that suite different needs. A lot depends on what this service is and how it needs to be passed around the application and how long it lives for. A lot of frameworks already give you good patterns for this.For instance axum has a way to pass around state to handlers, generally you pass ownership of the resource to axum and let it clone as required. Typically this means using an
for things that are expensive to clone. Though a lot of types you share this way (like database connections) deal with that internally and so are already cheap to clone in these usecases.
While we could have written this as a service where UserRepo is an injected value, doing so would introduce the complexities we’ve already explored
Does it? OOP style methods are basically just syntactic sugar for the most part. You can largely interchange functions and methods all you want to. Also, pure functions does not mean to most people to what your example is showing. Typically a function is considered not pure if it modifies any of its arguments. So by taking a
&mut
as an argument you make the function not pure.Given that I can only assume you mean raw functions vs methods on types. At which point there is not as big a differences as they think. For instance, their example of
async fn handle_session_completed( user_repo: &mut impl UserRepo, session: &CheckoutSession, ) -> anyhow::Result<()> {
Can easily be written as (well, at least if you ignore the issues around async traits which is a technical limitation that is being worked on, for now the
#[async_trait]
macro helps a bit, and hopefully soon this will be solved at a language layer)trait UserRepo { async fn handle_session_completed( &mut self, session: &CheckoutSession, ) -> anyhow::Result<()> {
And this form can even be called like a the former:
UserRepo::handle_session_completed(&mut user_repo, &session).await
There is little difference between a method on an type and a function that takes a argument to a type in rust. At least not in terms of how that author talks about them. Though I would lean towards the raw function here due to the async issues with traits. But in a lot of non-async contexts both are equally nice to use and would probably lean more on the trait/type method instead. More and more I just think of methods as type namespaced functions more than anything else, that is basically how you use them 95+% of the time.
But yeah, overall don’t write any language like it is a different language. Same goes for trying to write java like it is C or python like it is rust. Learn the patterns of the language you are using.
Wow, man, I forgot just how object-oriented Java is. You’ve got all these services pretending to run independently, except they’re not actually running asynchronously, and every service has a pointer to all the other services they need to talk to, leading to a huge tangled net of cross-dependencies. That’s why everything needs to be an interface, so you can mock it in tests.
Rust is a lot more …tree-shaped, with the main passing data into functions which call other functions and those return data, which can be passed into the next function.
Obviously, you can also build services running independently, but it’s usually done a lot more intentionally, by spawning own threads, passing around an (explicit) Arc, and then because you’re actually running asynchronously, you do need mutexes and such…Don’t tell me how to live! I can make mistakes on my own!
@snaggen coming from 10 years of Go, Rust is an ugly, unnecessarily complicated language with even more dogma than Go.
I guess for someone coming from C++ it must seem like an upgrade.
But as someone using Go on the server side , it’s just much more overhead involved and for what, potentially slightly better performance?
I went Go because it got rid of the mental burden of OOP.
And while it’s not perfect by far, it’s good enough.
Simplicity wins.I tried, time and again to like Rust.
It starts with error messages not making any sense.
How would I know that I’m missing an import when typing a demonstration from some website?
What’s up with the ugly ( || keyword) syntax or :: or .unwrap() .
Because of ownership you’re forced into certain hierarchies, which make the code ugly and hard to read.
There’s a bazillion libraries, but all are \I see where you come from. I first turned to Go, but at the end of the day it was a nice language but it didn’t tick my boxes. One of my main issues for backend servers, is for them to be robust. They should never fail in runtime. That means error handling is quite important, and as few runtime errors as possible. Go, fails this hard. The strictness of Rust, may be a pain while learning, and some syntax may be less than optimal, but the result will almost never fail in production. I have in fact never had a backend I wrote fail in production. The error handling also makes it easy to find any errors due to things out of my control. I switched a project from Java to Rust, and the log shrunk by a factor 10.
Note, I still use Go sometimes, but it is not my go to language. And on a second note, that you point to .unwrap() indicates that you never really used rust to write a backend, since use of .unwrap() will panik if you use that. You normally use .unwrap_or(…) or some other better construct. Also, complaining about unwrap() would indicate that you prefer a null pointer issue? Because, dropping null/nil is one of the great design choices, having null/nil is just a recepie for getting a runtime crash.
I’ve used Go for a similar amount of time as you. I started with Go 1.0 when I pitched it to my company at the time, and then migrated all of our BE code to Go. It solved the problems we had at the time, and I certainly don’t regret that decision.
However, I ran into a ton of issues that I really don’t think I should have. For example:
- dumb bugs stemming from
nil
and weird interaction with interfaces (e.g.interface{}((*int)(nil)) != nil
); honorable mention, functions attached to nil types can still be called, so the source of the nil could be hard to find map
isn’t thread safe; why??- nothing like
Arc
in Go, either use a channel or DIY with a mutex
And so on. Go strives to be easy to write, but it doesn’t do much to hide the footguns. So it’s like C, but at least you get panics instead of SEGFAULTs.
These days I much prefer Rust. I followed Rust pre-1.0, and I’ve used it a bit for personal projects since 1.0. It has come a long way, and I think it’s finally at a point where the barrier to entry is low enough and the ecosystem is robust enough that it can be a decent alternative to Go. I like that it doesn’t hide the complexity and forces you to deal with design problems at compile time instead of running into them randomly.
If Rust is too much, I prefer Python.
I wish Go would do something about its footguns. I honestly don’t like using it anymore because I get a ton of complexity with goroutines and whatnot, and very little to help manage it. The main thing I miss is
pprof
, and I find I haven’t needed it with Rust because things just run better.@sugar_in_your_tea map not thread safe, because multiple threads iterate over one map so you have to use sync.Mutex to lock it for reading, writing or both. I fell into that pit too. Part of the learning process. C++ has mutexes too.
Right, the whole point of Go is to be concurrency friendly, as in, the main reason you’d use it is to do multi-threaded concurrency. That’s the #1 selling point and it’s why goroutines and channels are a thing. Yet there are so many little things you need to keep in mind, such as:
- sending mutable types across channels - you either need to ensure it cannot be used by the sender until the receiver is done with it (either by coupling the logic, or by destroying the sender’s copy), or you need to copy it; then you get into whether you need a deep copy, and how to protect any other reference types you send; Rust solves it with the
Copy
andSend
traits, which can only work if everything you depend on implements theCopy
orSend
trait - no scope guards, so you need to rely on
defer
for unlocking; in Rust, you get this for free, as soon as you lock something, it’ll unlock once your scope exits - a read from a
nil
channel blocks instead of panicing (ideally it would fail to compile) - that’s a pretty easy mistake to make
Part of the learning process
Unfortunately, you hit a lot of these cases in production instead of at compile time. In Rust, many of these types of issues are caught before you even get to testing your code, much less actually trying to ship something.
That’s why my decision process is something like this:
- Python - if I just want a quick prototype and don’t particularly care about production-level stuff like performance, safety guarantees, etc
- Rust - once I need performance, safety guarantees, etc
Go isn’t easy enough to just throw a junior developer on a project, and it’s not robust enough to catch a senior developer when they make mistakes. I thought it would’ve been good enough, but it’s just not as productive for me as Python (generally you hand-wave away the concurrency by using separate processes), and the compiler doesn’t catch nearly enough to rival Rust, so I’m happy to pay the productivity penalty to shift from Python to Rust once I know I need something a bit more serious. And once you’re experienced, the compiler doesn’t really get in the way anymore.
Go is interesting I guess as a microservice tool where you’re making things that are small enough, but imo it really doesn’t scale all that well in terms of reducing bugs as the project gets larger.
- sending mutable types across channels - you either need to ensure it cannot be used by the sender until the receiver is done with it (either by coupling the logic, or by destroying the sender’s copy), or you need to copy it; then you get into whether you need a deep copy, and how to protect any other reference types you send; Rust solves it with the
- dumb bugs stemming from
I disagree. As someone that learnt go first then rust I much prefer rust in almost every way. The more I learnt Go the more it bothered me, so many promises it made were broken and so many good ideas half implemented. And the more I learn rust the more I enjoy it, it fixed most of the issues I had with Go and fixes a lot of issues I constantly see in Go code in production settings.
How would I know that I’m missing an import when typing a demonstration from some website?
I don’t see how this is an issue? You have a
use somecrate
at the top of a file that tells you you need something external, demonstations online will only really have crate use statements so it is never really a problem to tell. If they are missing? Well, go has the same but worst problem as you cannot easily guess the import you need for it as you need a full url.What’s up with the ugly ( || keyword) syntax or :: or .unwrap() .
No more ugly than
func() { ... }
or
if err != nil { return err; }
Because of ownership you’re forced into certain hierarchies, which make the code ugly and hard to read.
I dont know what you mean by this? The code rust encourages you into IMO is generally far more readable and less bug prone than a lot of languages.
Rust is a much harder language to learn and get into. But I still find it gets better every day and you learn better ways go doing things. In go if there is something you don’t quite like you re typically stuck doing it the one go way in every situation.
Sounds like you don’t understand Rust. It’s more difficult to learn than Go. Go can be picked up by an experienced developer in a day. Mostly becauee there isn’t much in the language so there isn’t much to learn.
Try learning Rust properly before writing code. Learn the concepts. It’s not Python where you hack something together that maybe kinda works.
@crispy_kilt correct I don’t understand it and I don’t want to anymore, because I’ve seen it’s much more complicated than what I already use, but I have written that already.
You’re acting like a kid that’s butt hurt that someone said something bad about their favorite team.
Rust promised performance but for the cost of much more mental overhead and more complicated and indoctrinated workflow.
I do what’s best for me.
Don’t like it idgafOf course, do what’s best for you, I didn’t mean to suggest otherwise
Because of ownership you’re forced into certain hierarchies, which make the code ugly and hard to read.
For non-gc languages you always have ownership, in most languages you just have to keep track of it manually. And whenever the rust compiler gives an error, you would most likely have had a future issue in another language. For gc languages, this may still exist if you share data between threads, causing possilbe race conditions and data corruption. So, the ownership/borrow model is just a formalization of implicit rules that exists in most languages.