Thinking in trees and lines, formatting Rust

5 minute read Published: 2023-09-29

I see my code projects in trees and lines, and it affects how I want it to be formatted.

Lines

When I see code, or configuration files, or any user editable data, I see lines, and I first think in lines. This approach brings a lot of advantages:

First, it's easier for the human to parse code when each item is on its own line, as in a list.

Then, diffs, so essential today, make sense, because the modified lines match the modified items; the essence of the difference isn't hidden by the noise of unchanged items.

With one item per line, code is less wide, and better fits a thin column. All papers editors know that the eye scans text more easily in thin columns. It's also easier to work with thin columns when you display a lot of texts on the same screen (some codes, some logs, compilation reports, etc.).

Many code editors are also much more efficient when you manipulate lines.

So, lines it is. But not just lines:

Trees

When speaking of imports, what we have isn't just lines but a tree, similar to a filesystem's tree, and of course to your project's module structure.

I like to have the tree visible. It helps me see the consistency and logic of the imports. I thus want to see my imports as a clear tree, one item per line, and the different levels of paths clearly displayed.

This is quite similar to what you see in broot, there's no coincidence, this is how I think.

Formatted imports and functions

With this acquired taste, here's how I like my imports and function to look in Rust.

Imports:

use {
    crate::{
        pattern::*,
        verb::{
            Internal,
            VerbInvocation,
        },
    },
    bet::BeTree as Tree,
    std::{
        fmt,
        fs::File,
        io::{
            self,
            BufRead,
        },
    },
};

Function arguments:

pub fn add_verb(
    &mut self,
    invocation_str: Option<&str>,
    execution: VerbExecution,
    description: VerbDescription,
) -> Result<&mut Verb, ConfError> {

Trees are visibly trees, and you get used to recognize what's imported. And when an item is added, removed, changed, either in imports or among function arguments, only the relevant lines are touched.

Auto-formatting

Can we get a satisfying formatting with cargo fmt ?

First, let's be honest, the answer to such question will be "not bad, but not very good".

It produces the blocks of code above, but it's not perfect otherwise.

Rustfmt lacks some configuration options.

For example, I'd like to get this for consistency:

use {
    crate::{
        verb::{
            Internal,
            VerbInvocation,
        },
    },
};

Or maybe this (which is a little less heavy):

use {
    crate::verb::{
        Internal,
        VerbInvocation,
    },
};

But what I get is this:

use crate::verb::{
    Internal,
    VerbInvocation,
};

Yes it's compact, and it may be faster to read, but it almost never stops there, I have to add imports from other modules, and then the whole block changes.

But that's minor.

The main problem is that it too often breaks consistency.

For example it will produce horrors like this:

match self.exec_mode {
    ExternalExecutionMode::FromParentShell => {
        self.cmd_result_exec_from_parent_shell(builder, con)
    }
    ExternalExecutionMode::LeaveBroot => self.cmd_result_exec_leave_broot(builder, con),
    ExternalExecutionMode::StayInBroot => {
        self.cmd_result_exec_stay_in_broot(w, builder, con)
    }
}

where a human coder would have chosen a consistent formatting for the whole match, which would have made it easier to read. This happens all the time, everywhere.

On some projects, I can't resolve to switch to cargo fmt, and it's OK because I'm the only one writing code, and readers don't notice because it looks very similar to the standard formatting.

But you can't really avoid it as soon as you're not alone on the project.

When auto-formatting is needed for the project, I get something satisfying enough with this rustfmt.toml:

edition = "2021"
version = "Two"
imports_granularity = "one"
imports_layout = "Vertical"
fn_params_layout = "Vertical"

It's a short set of rules, because you have to be cautious when departing from the standard.

imports_granularity = "one" gathers all imports in only one use block.

imports_layout = "Vertical" ensures there's only one import per line.

fn_params_layout = "Vertical" ensures there's never two function arguments on the same line in function declarations.

That's what I use when I expect contributors, for example on bacon.

And what if you don't want to change the rustfmt rules ?

Formatting settings are always a compromise. When you're not doing most of the code, everybody should feel happy with any step away from the defaults. I don't push for some formatting rules when other code owners are uneasy with them.

What to do, then, when the rustfmt rules are the default ones ?

What you can have is some shortcut to format the current file, either just for reading, or for editing.

When I want to clearly see the imports, I just hit spacefi (format import) which I configured in vi like this:

nnoremap <Leader>fi <Esc>:!rustfmt --config imports_granularity=one,imports_layout=vertical --edition 2021  %<CR><CR>

Depending on the project, I could add fn_params_layout=Vertical too.

After I did my changes, the standard project rules will apply (for example on commit hook).