Programming Style

Congratulations, youve found a secret level!

This is a super work-in-progress page which collects various rules-of-thumb I use. The primary goal so far is to collect the rules for myself, thats why I dont link to this page from anywhere yet.

General

Naming

Prefer full names except for extremely common cases (ctx for context), or equal-length pairs (next/prev). Use consistent names. Naming variables after types (let thing: Thing) is a way to achieve global consistency with little coordination.

Build a vocabulary of standard names and re-use it:

ctx

context of an operation. Typically holds something mutable. Read-only context is named params.

params

A bag of named arguments. Unlike config, might hold not only pod types.

config

Generally user-specified POD parameters.

sink

output of an internal iterator, typically sink: &mut FnMut(T) or sink: &mut Vec<T>.

lhs, rhs

operands of a binary operator.

fuel

Recursion and infinite loop guards

result

A return variable.

line_index, line_number

Index is unambiguously zero-based. By convention, number is one-based.

Equisized pairs;

  • add/sub, mul/div
  • lhs/rhs
  • s/e
  • next/prev
  • source/target
  • src/dst
  • index/count
  • insert/remove
  • beg/end
  • fresh/stale

Explicit Data Tables

Remove code duplication by extracting commonalities into tabular data

// GOOD
const cases = ["foo", "bar", "baz"];
for case in cases {
    if x == case {

    }
}

// BAD
if x == "foo" {

} else if x == "bar" {

} else if x == "baz" {

}

Bulk IO

Avoid opening file descriptors in favor of bulk operations. To write data to a file, you need to follow a lifecycle: open file descriptor, issue write syscalls, close file descriptor. Lifecycle handling requires complicated type-system machinery and is bettre avoided. Usually, standard library provides something like std::fs::read_to_string which encapsulates lifecycle management.

Rust

No Self Types

Write types out explicitly, avoid Self alias if possible:

// Good
pub struct Diagnostic {
    pub code: DiagnosticCode,
    pub text: String,
}

impl Diagnostic {
    pub fn new(code: DiagnosticCode, text: String) -> Diagnostic {
        Diagnostic { code, text }
    }
}

// Bad
impl Diagnostic {
    pub fn new(code: DiagnosticCode, text: String) -> Self {
        Self { code, text }
    }
}

Rationale: reducing cognitive load, optimizing for the reader. Resolving Self is a small mental effort, it can be avoided.

Prefer new Over default

Use new over default to construct instances.

Rationale: new is too ingrained.

Blank Line Between Declarations

Leave blank line between top-level declarations:

// Good
impl Foo {
    pub fn foo() {
    }

    pub fn bar() {
    }
}

// Bad
impl Foo {
    pub fn foo() {
    }
    pub fn bar() {
    }
}

Rationale: consistency. Omitting blank line leads to somewhat terser code, but is very hard to do consistently.

Derive Order

Use the following order of derives:

#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]

Rationale: consistency. Debug comes last because it is the most often added item.o