Categories > Coding > Rust >
Rust Best Practices 1 — Case, Enums, and Argument Flexibility
Posted
Hi everyone. I want to talk about best practices in Rust.
Rust is an incredibly designed language with influences from across the board. These influences have resulted in a unique set of idioms that define what idiomatic Rust code (known as rusty code) looks like. For people coming from the object-oriented or imperative world, many of Rust's idioms are likely to be non-obvious or unnatural, and they will find themselves writing unidiomatic code that is closer to home: while this works, you will encounter resistance and pull requests from the Rust community if your library is written in this way because you are undermining Rust's benefits.
---
First, let's begin with what is quite a "turn-off" for people looking to use Rust:
https://cdn.discordapp.com/attachments/1075783615671189534/1089084526371815464/230abc963298c730.png
The compiler is pedantic about the case you use, enforcing a specific style. Many people see this as having their freedom stripped from them, but that is arguably a good thing; the crate (Rust term for a package) ecosystem having a sane, unified, and readable style makes the development of programs much easier as you always know what to expect, directing the compiler to allow this warning may be tempting to some. Still, I assure you of the benefits of just switching. You can dismiss the compiler's non_snake_case with an allow directive for FFI purposes, as not all languages have the same naming convention as Rust.
I advise everyone to use rustfmt (accessible through "rustfmt" or "cargo fmt") on your code, you might not like some things, but it's in everyone's benefit that you do so. If there is something that it's proposing that is objectively bad such as verticalising an enum with many variants, which results in it spanning hundreds of lines, you can place the rustfmt::skip attribute on it.
---
Second, let's see how Rust can help us get our programs on rails:
Let's say we have a single function that lets us interact with our pet, which can either be a dog or a cat, and we can feed the pet or play with it. A naive implementation looks like the following:
First, let's write a struct and a constructor:
https://cdn.discordapp.com/attachments/1075783615671189534/1089084785705619466/6158043d9becb4ff.png
Next, let's write the interact function, which takes a String argument to specify what kind of interaction we want to do and a u8 of the amount:
https://cdn.discordapp.com/attachments/1075783615671189534/1089085142590570506/a1690a78025aca8e.png
I don't know if you see it, but this is awful, but let's act like we don't know any better. Someone who knows a little bit more than us about Rust would then suggest this refactor:
First, he changes the struct definition and uses larger, signed integers to reduce the chance of an overflow:
https://cdn.discordapp.com/attachments/1075783615671189534/1089085667415425095/376412f1237b3e6f.png
Next, he uses the Result type in Rust, which represents success or failure without the use of exceptions. Its definition looks like this:
https://cdn.discordapp.com/attachments/1075783615671189534/1076159566812827728/image.png
He then uses the guard clause pattern to extract out the conditions and remove nesting, and ends up with his code, let's look at his new function first:
https://cdn.discordapp.com/attachments/1075783615671189534/1089085847262986250/3fe61426833507fd.png
The story is similar in his interact function, where he needs to cast between integer types to calculate the result:
https://cdn.discordapp.com/attachments/1075783615671189534/1089086141703131166/1f28a8f5abd1940d.png
What a ride, that was bad. It was really bad. Any of these functions could fail for no good reason, some people suggest using a boolean value or an integer, but our scenario is an utterly non-obvious use of said types; there has to be a better way, and there is.
First, I sort the fields alphabetically, then revert them to u8. You'll see why that isn't an issue later. Note the type PetKind; no, it isn't an alias for bool. You'll see in a second:
https://cdn.discordapp.com/attachments/1075783615671189534/1076157489919311912/image.png
Aha, see, I created my enumerated types (tagged unions) for both pet and interaction kinds:
https://cdn.discordapp.com/attachments/1075783615671189534/1076157364723515412/image.png
As a result, we now have a beautiful one-liner infallible constructor that uses an implicit return:
https://cdn.discordapp.com/attachments/1075783615671189534/1076161706356654152/image.png
And an infallible interact method that uses saturating arithmetic to prevent overflowing (the values will max or min out at 0 or 255 and stay there until you add or subtract, respectively):
https://cdn.discordapp.com/attachments/1075783615671189534/1076158456840597535/image.png
---
Finally, let's look at function arguments:
People need to have more flexible function arguments. Look at this example:
We're trying to display a death message when some skrub dies, so we sip mountain dew and write this:
https://cdn.discordapp.com/attachments/1075783615671189534/1076169444759437312/image.png
That obviously (or not so obviously) doesn't compile because string literals are &str (&'static str), and the function wants a String. Someone smart will say that it's better to accept &str because you can coerce &String into &str like so:
https://cdn.discordapp.com/attachments/1075783615671189534/1089086604225822760/10b30b453a79c507.png
But now we can't take String itself, so what we can instead do is use the Into trait and accept anything that you can convert into a String, then call Into::into on it like so:
https://cdn.discordapp.com/attachments/1075783615671189534/1076172558795284611/image.png
This last example is not good Rust because you shouldn't be very generous in spreading Into around your code. The point is that if you want something specific, ask for it directly. For this example, requesting &str is the sweet spot: your game should not have many different string types floating around; accepting a reference is okay because we do not intend to modify, so why not accept &str to accept literals directly as well and to take advantage of the coercion and not an explicit call to Into::into. The generic also monomorphises, increasing code size as the number of types used with the function grows.
---
Don't fight, or I'll lock. The upcoming best practices post will be about iterators and error handling.
Rust is the future and the future is now
Replied
Ewwww rusty code. All jokes aside you really wrote a lot which is props to you and beneficial to those who need this. I don't need it as I am doing more web development but still thank you for sharing something to this community this is a very big contribution.
Cancel
Post
https://media.discordapp.net/attachments/1013939973671624917/1027279180192292944/unknown.png
https://media.discordapp.net/attachments/1010670716062007347/1108945330847883274/image.png
Replied
Wonderful thread, thanks for educating people on better Rust practices.
Hope this thread inspires some new programmers to try Rust or at least check it out.
Cancel
Post
Users viewing this thread:
( Members: 0, Guests: 1, Total: 1 )
Cancel
Post