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 or if/else? How should errors be handled? Are macros OK? Yes, the clippy 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.
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 iflet enough, other times a match 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.
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?
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.
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)
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.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).
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.
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?