Best Practices to write Rust code

Hi Rust Community,

Can someone share the link where the best practices to write and maintain Rust code and folders are listed?
For eg;
For a given project, if we are using multiple structs is it considered a best practice to create types.rs, and for the implementation of that types we create impl.rs?

4 Likes

It's not a hard rule, but often, a struct and it's impl blocks are in the same file. This can be seen for example in rust-base64/src/alphabet.rs at master · marshallpierce/rust-base64 · GitHub

3 Likes

I would also like to know some resources. In Elixir for example we have many resources like that … Firstly the documentation describes some naming convention that are used in language core and therefore are recommended to use in other places as well. We have also a credo (static code analysis tool often used for code consistency) and it's style guide. Are there any resources like those linked created for Rust language?

1 Like

I would say certainly not. Mostly I see that there is some functionality that is extensive enough to be spread over multiple functions. That functionality often requires some data shared between all those functions. So it makes sense to put that data into a struct and make the functions methods implemented on the struct.

With that in place it makes little sense to put the struct and the implementation into different source files. We are not dealing with the .c and .h files of C, that is not necessary.

Sometimes I find I have plain old data getting passed around the place, as parameters or as messages in channels. That is data only, no methods. Then I start to wonder where I should define those structs. I think a good idea there is to define the structs in whatever module creates them. Although I have sometimes put message structs into their own ,rs file.

3 Likes

Sounds like what cargo fmt and cargo clippy are for. Given that Rust does such a lot of checking everything at compile time is there really a need for anything more?

4 Likes

Rust code is organized into modules according to functionality (internal components) and encapsulation (since a module can have private members). So the question falls back on you: How do you want to break up the functionality of your app or library? Or another way of asking is: What APIs do you need between internal components? The language doesn't dictate this, nor are there any conventions, since this sort of design can be different for every project. If you asked this question about a specific type of app, however, a microservice with an HTTP API, for example, there may be a more specific answer from those who are experienced with this type of app.


Sometimes the answers to the above questions are not known right away, but become clear as you implement and test your app. So often the best thing is simply to start with a single module, then break it up as you discover what internal components and APIs you need. It is relatively easy to refactor Rust code and break it up into modules.

9 Likes

Yeah, it looks like that cargo fmt is equivalent of mix format and cargo clippy is equivalent of mix credo. It looks like that some naming convention are mentioned in Rust documentation:

This chapter covers concepts that appear in almost every programming language and how they work in Rust. Many programming languages have much in common at their core. None of the concepts presented in this chapter are unique to Rust, but we’ll discuss them in the context of Rust and explain the conventions around using these concepts.

Source: Common Programming Concepts - The Rust Programming Language

I didn't read all of it yet, but let's say that more or less they cover 3 of 4 resources mentioned by me. The last one is style guide. In short it covers everything which is not covered by linter checks like common naming patterns. It gives one or many ways how to write same code in preferred and bad way, for example:

# preferred way - common suffix Error
defmodule BadHTTPHeaderError do
  defexception [:message]
end

defmodule HTTPRequestError do
  defexception [:message]
end

# also okay - consistent prefix Invalid
defmodule InvalidHTTPHeader do
  defexception [:message]
end

defmodule InvalidUserRequest do
  defexception [:message]
end

# bad - there is no common naming scheme for exceptions
defmodule InvalidHeader do
  defexception [:message]
end

defmodule RequestFailed do
  defexception [:message]
end

Also I believe that such resource would be awesome addition for new Rust developers:

as it's quick and provides a graphical visualization of regular expressions of valid naming. It's short form is really handy if you search for a specific information.

btw. This form also recently has started to be adopted in core documentation, for example see: Enum cheatsheet — Elixir v1.17.0-dev

1 Like

The API Guidelines explains most of Rust best practices, including naming. That, rustfmt, clippy, and some basic knowledge about other crates and std is all you really need to know.

13 Likes

Rust's module visibility is mostly tree-structured, so a decent rule of thumb is "could you copy-paste this logic into a separate crate?"

Applied recursively, you should get a tree structure, where everything only depends on the contained modules and a minimized "public" surface api of other modules.

This also means you have an easy path to breaking up the crate when you want to look into improving build performance.

4 Likes

No, that's useless.

You shouldn't structure your code around superficial language features. You should structure your code according to functionality.

5 Likes

Definitely agree. In Elixir language in Phoenix framework we have a concept of context to group modules around specific set of features like account context for user, preferences and all auth stuff or organization context for company, member and so on …

That's important as your boss is not interested how you have implemented something as long as it match project spec and it have a good performance. The bug reports usually look like please fix company invite process. I got error: …. It's more clear for your team where some code is located when they are navigating by a (group of) features rather than as said "superficial language features".

I’d like to define all traits needed in a crate which may be named xx_core, and ensure the traits work with each other. This helps achieving generic code and organizing good doc tests. And then implement those traits in another crate named xx_utils.

After all, I have traits in xx_core, implementations in xx_utils, proc macros in xx_derive. Organize them with workspace, and easy to publish to crates.io.

All because traits and struct may both be seen as type, I just separate them apart by putting them in different crates.

I’m not a 大佬 in Rust, but from my perspective, this could be ok.:rofl:

(As I'm sure you know, but to clarify) due to the orphan rule, you can't have a crate just with trait impls; so you end up with something like:

// foo_core
pub trait Foo { ... }

impl Foo for std::* { ... }

#[cfg(feature = "popular-crate")]
impl Foo for popular_crate::* { ... }

// foo_utils
pub struct SomeUtil { ... }

impl Foo for SomeUtil { ... }

// and if it makes sense, add an extension trait
trait FooExt: Foo {
  fn some_util(...) { ... }
}
impl<T: Foo> FooExt for T {}

Ideally, you could have specific crates for each trait impl if it's plausible a user would only want one, but the rough pattern is good if you want to establish a "vocabulary" with the trait.

Thank you for your reply!

However, I’m not a native speaker and your expression is too hard for me to understand especially the last sentence. I even cannot understand whether you agree with me or not.

Do you want to show this drawback?(For example, we want to create a CLI tool)

// core
trait Foo {}

trait FooExt {} 
impl<T: Foo> FooExt for T {} 

// utils
use core::Foo;
impl Foo for FooImpl {};

// cli
use utils::FooImpl; // If we want to use FooImpl
use core::{Foo, FooExt}; // we should also use the traits in core
                   // this is tedious maybe

…

Here is my project which separate traits and utils apart. I found the core can be useful for others, but utils is not so convenient.
However, the final code is really in good Rust style, and well documented easily. During my development, I worked carefully on what trait I need and only wrote the function signature on core stage. It’s so relaxed. After adding some Ext traits, all I need is just implement them on utils stage, then compose them into a CLI, the last two were quite fast to be done.

Lastly, due to Rust’s generic supporting, now I think it is ok that trait does almost everything, as the struct just working as an entry, all I need to do is to compose the traits onto a struct which stores essential data.

Maybe I will find something wrong in the future, and this reply may be more related to coding habit instead of workspace organization, I feel sorry about my going away from the topic of this question.

It's fine, I was agreeing in general, just pointing out for new Rust users that you can't have a crate with just trait implementations; they need to be either in the same crate as the type or the trait.

The last paragraph was just mentioning that for some uses, you probably want a more specific crate than just a "utils" for all of your implementations.

As an example, if your CLI application defined a Config trait, then you might have a bunch of generally useful utilities like "load from current directory" or "merge" that operate on any Config implementation, which make sense to be in a general "utils", while you might have more specific implementations for, say YAML and JavaScript that should be in a separate crate each, because a user is likely to only want one of those.

Another way to structure it is a crate with utilities for people that are implementing a trait, rather than using it. There's no one correct approach!

2 Likes

Brenden Matthews is writing a book on 'Rust Design Patterns'. It discusses some of this information. Perhaps it would be helpful. It is is expected to be released this fall, but early access is available.