Note: This post is about how I arrange the code I write in Rust. If you wanted to “order” Rust code in the “hire someone to write code” sense, you should still keep on reading as this is excellent material for a job interview. (Not the opinion I present but having an opinion on the topic.)

Arrange code to be in the suggested reading order

I try to order the functions/modules/items in my source files going from most high-level to most-concrete/small-scope. You might call it fn main first.

I do this so that when someone opens the file and starts reading they can get the general idea of what this file is about very quickly and then, if needed, dive into the details they are looking for. I think this works especially well for the entry files in a project.

The opposite position would be to start with the generic helpers, then introduce domain specific types and functions, and finally have a main function that calls all the things you’ve defined above it. I tend to read these files by scrolling to the end and then moving upwards; so it just feels weird to someone used to reading European languages.

This is not necessarily the order I write code in

I typically don’t sit down, write code from top to bottom, and end up with a perfectly structured and arranged file. That is not the goal at all: It’s easy enough to copy and paste parts of a file into another, or use editor/language plugin features to quickly navigate between sections of files when I’m looking for something specific. Remember: My goal of arranging code in the way described above is for when you read it for the first time.

Some specifics about Rust code

The order of items usually doesn’t matter in Rust (macros are a weird edge-case). There are some things to decide though:

How to order type definitions (struct, enums) and their implementations?

There are two obvious choices:

  • Define all the types first and then list all the implementations?
  • Interleave the implementations with the types?

I personally am fine with both, and tend to go with the latter. I might however split impl blocks up and define some methods (especially private ones) right next to the functions they are needed for. (This sometimes feels like ad-hoc single-instance traits.)

@matklad had another interesting comment:

[I] love to read types upfront (if you know the set of fields, you know all potential methods that can exists)

Where to put use statements?

To use an item (type, trait, function, etc.) that is not in scope you can either refer to it by its full path (e.g., std::collections::HashMap) or import it using use std::collections::HashMap; after which you can refer it as just HashMap. The issue is: Where to put these use statements?

One typical approach is to put them all at the top of the file. This “wall of imports” is what you also see in many other programming languages. This is a good idea if the only location you can put import statements is at the root level and especially if the file contains one “main item” (e.g., if foo.java contains import statements followed by class Foo { … }). In Rust, however, you don’t often have just one item at the root level. You have a foo.rs that contains a struct Foo { … }, various impl Bar for Foo { … } blocks, possibly some free functions, and in many cases even unit tests. So, we should rethink where to put these use lines!

One approach I’ve taken previously is to keep uses as close to the area they are needed as possible. If I have a function that reads five files, I add a use std::fs::File; at the beginning of that function. Sadly, this breaks down when you want to import a type to use it in a function’s/method’s signature or as a field type in a struct: In that case, the use use needs to be on the level above the usage point (i.e., on the level of the function/trait/struct definition). Additionally, if you have use std::sync::Arc above one struct, it becomes available in the general scope. So, your next struct that uses Arc doesn’t need to have a second instance of that use line.

This all lead me to the point where I often just go back to collecting uses at the top of the file. I will however not write single use lines for all the items I want to use, but instead

  • only import the module when I use various items but each of them only a few times (e.g. use std::sync; and refer to sync::Arc and sync::Mutex.)
  • make use of Rust’s nested imports (e.g. use std::{error::Error, fs, io::{self, Read}};).

Split public and private interfaces

When the entry file of a package or module1 gets to long, you want to split it up. A solid approach is to move implementation details are in a separate file, which might end up being an “helpers” file or actually be most of the code split up in modules that are not exposed to the outside.

In languages that allow specifying the visibility of items on a very granular level you can very precisely mark only parts of your code as “public interface”. But this means also means that there is a non-public interface: Indeed, most abstractions have two interfaces: A public, consumer-facing one, and an internal one, for “producers”. Consciously separating the two by the layout of your code will help create maintainable and comprehensible code bases.

Abstractions on top of abstractions

Often, you end up writing structures that are only used internally but then get converted into other structures for the consumers of your package/module. I don’t have a good recipe on how to deal with that, except that I would recommend trying to the boilerplate/conversion part “obvious”/invisible and thus highlight the differentiating details.

  1. “Package or module”? Yes, and also “application” and “function”: This is a fractal property.