Programming Style
Congratulations, you’ve 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, that’s why I don’t 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)
orsink: &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
- prefix/suffix
- receive/consume
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:
Rationale: consistency. Debug comes last because it is the most often added item.o