An F# Code Standard to achieve Correctness, Consistency, and Simplicity -
A) Correctness
-
Set warning levels to the highest level possible in all projects.
-
Enable warnings as errors in all projects.
-
Enable
--warnon:1182
(warn on unused variables) in all F# projects. -
Prefer immutable types and referentially-transparent functions.
-
Make illegal states unrepresentable when feasible Here's our friend Scott Wlaschin on the subject.
-
Avoid trading away exhaustiveness checking unless you have a specific need.
-
Avoid trading away type inference unless you have a specific need.
-
Avoid creating object and struct types, as well as instance members, and properties for all but the most trivial getters, unless you have a specific need (such as for creating a plug-in, a DSL, for interop, for efficiency, or etc).
-
Try to preserve debuggability of code by -
- introducing local bindings to potentially-interesting intermediate results,
- avoiding unnecessary laziness and asynchrony (but since async being strewn throughout code is rarely avoidable, consider using the Vsync monad instead).
-
Suffix option bindings, choice bindings, either bindings, and bindings to potentially null values with
Opt
. -
Prefix functions that return an option, choice, either, or potential null with
try
. -
Try to use unique names for public fields and discriminated union cases to avoid ambiguating type inference. For example,
Id
is not a good public field name, butProductId
might be.
B) Consistency
-
Use 4 spaces for indentation, not 2, nor 3. 5 is right out.
-
Use column 120 as the line length limit where practicable. Column 120 is not a hard limit, but is elegantly achievable in most cases. A common exception to this rule is for code that constructs error messages.
-
Use the standard F# naming conventions by -
- using
UpperCamelCasing
forNamespaces
,Modules
,Types
,Fields
,Constants
,Properties
, andInstanceMembers
. - using
lowerCamelCasing
forvariables
,functions
,staticMembers
,parameters
, and'typeParameters
.
-
Use shadowing on different bindings with the same conceptual identity rather than
'
suffixes (this helps correctness significantly). Conversely, avoid shadowing on different bindings with different conceptual identities. -
Place
open
statements at the top of each file, right below the current namespace declaration. -
Order the parameters of functions from least to most important (that is, in the order of increasing semantic impact). This makes currying easy to leverage and consistent.
-
Prefer stepped indentation as it refactors better, keeps lines shorter, and keeps formatting normal and enforcible via automation. For example, write this -
let result =
ingest
apple
banana
grape
- rather than this -
let result = ingest apple
banana
grape
- F#'s syntax is based on ML, which is structurally derived from Lisp rather than C, so use Lisp-style bracing instead of C-style. For example, write this -
let ys =
[f x
g x
h x]
- rather than this -
let ys =
[
f x
g x
h x
]
- and this -
type T =
{ M : int
N : single }
- rather than this -
type T =
{
M : int
N : single
}
- Tab out discriminated union case definitions to keep them lined up with their members. For example, write this -
type T =
| A of int
| B of single
static member makeA i = A (i * 2)
static member makeB s = B (s * 2.0f)
- rather than this -
type T =
| A of int
| B of single
static member makeA i = A (i * 2)
static member makeB s = B (s * 2.0f)
- Handle the intentional case first when matching / if'ing -
let fn valueOpt =
match valueOpt with
| Some value -> // do what we actually intended to do in this function
| None -> // handle the edge case
- Surround tuples with parens to keep evaluation ordering and intent clear. For example, write this -
let (a, b) = (b, a)
- rather than this -
let a, b = b, a
-
Conceptually, () is unit, so please treat it as such. For example, write
fn ()
rather thanfn()
. -
Conceptually, (a, b, c) is a tuple, so please treat it as such. For example, write
fn (a, b, c)
rather thanfn(a, b, c)
. The exception is when you need to use F#'s flow-syntax feature.
C) Simplicity
-
Use F# as a functional-first language, rather than an object-oriented one. Here's our friend Rich Hickey on why object-orientation in inherently complex..
-
For mutation that you can't avoid, try to encapsulate it behind a referentially-transparent interface wherever feasible. For example, consider wrapping your mutable constructs with KeyedCache or MutantCache
-
Avoid dependencies on untested, incomplete, or unnecessarily complex libraries / frameworks.
-
Avoid checking in dead / commented-out code. If unavoidable, leave a comment above the code explaining why it's commented out and / or when it will be useful again.
-
Consider passing around multiple dependency references in a single container (usually a record) rather than individually.
-
Consider making such a container an abstract data type by -
- privatizing all of its fields like so -
type MyContainer = private { ... }
- exposing a narrow set of static member functions that provide only the desired functionality in a more abstract way.
Here are some detailed slides on leveraging abstract data types here - Structuring F# Programs with Abstract Data Types (view presentation here - https://vimeo.com/128464151)