Introduction
This guide provides recommendations for writing idiomatic, readable, and maintainable F# code. Following these guidelines will help ensure consistency across F# projects and improve collaboration among developers.
F# is a functional-first language, and its style guidelines reflect this. Emphasis is placed on immutability, expressions, and composability.
Naming Conventions
Clear and consistent naming is crucial for code readability. F# conventions generally follow PascalCase for types and camelCase for values and functions.
Types
Type names (interfaces, records, discriminated unions, classes, etc.) should be in PascalCase
. They should be descriptive and singular if representing a single item or plural if representing a collection.
type CustomerInfo = { ... }
type OrderId = int
type ValidationErrors =
| InvalidEmail
| MissingField of string
Functions
Function names typically use camelCase
. For operations that transform a value without changing its fundamental identity (e.g., getting a property), use verbs like get
, find
, read
. For transformations that create a new value based on an old one, use verbs like map
, filter
, convert
.
let calculateTotal() = ...
let getUserById userId = ...
let mapOrderStatusToString orderStatus = ...
Values
Value names should use camelCase
. Avoid single-letter variable names except for common mathematical or loop indices (e.g., i
, j
). Local bindings are often inferred from context, but explicit names should be descriptive.
let customerName = "Alice"
let mutable counter = 0
let processOrder order = ...
Modules
Module names should be in PascalCase
and are often plural.
module Orders =
let getOrderById id = ...
module Services =
type UserService() = ...
Parameters
Function and method parameter names should use camelCase
and be descriptive. If a parameter name is the same as a type name, consider a slight variation (e.g., customerInfo
for a parameter of type CustomerInfo
).
let createUser (customerInfo: CustomerInfo) = ...
Formatting
Consistent formatting improves code readability and makes it easier to scan.
Indentation
Use spaces for indentation, typically 4 spaces per level. Align nested constructs consistently.
let rec factorial n =
if n <= 1 then
1
else
n * factorial (n - 1)
Whitespace
Use whitespace judiciously to separate logical blocks and improve clarity. Add spaces around operators, after commas, and between elements in sequences.
let sum a b = a + b
let result = List.map (fun x -> x * 2) [ 1; 2; 3 ]
Line Breaks
Keep lines reasonably short (e.g., under 100 characters). Break long expressions logically, often aligning with indentation.
let longOperation =
someFunction param1
|> anotherFunction param2
|> yetAnotherFunction param3
Idiomatic F#
Embrace F#'s functional nature to write concise and powerful code.
Immutability
Prefer immutable data structures and bindings. Use let
bindings for immutable values and mutable
only when absolutely necessary.
Tip: Immutability simplifies reasoning about code, especially in concurrent scenarios.
// Good: Immutable
let message = "Hello"
// Avoid if possible: Mutable
let mutable count = 0
count <- count + 1
Pattern Matching
Use pattern matching extensively for deconstructing data, handling different cases of discriminated unions, and control flow.
let processResult result =
match result with
| Ok value -> printfn "Success: %A" value
| Error msg -> printfn "Error: %s" msg
Type Inference
Leverage F#'s powerful type inference. Explicitly annotate types when it enhances clarity or is necessary for complex scenarios.
// Type inference handles this
let greeting = "Welcome"
// Explicit annotation for clarity or necessity
let sendEmail (recipient: string) (subject: string) (body: string) : Async<Unit> =
async {
// ... email sending logic
return ()
}
Pipelines
Use the pipeline operator (|>
) to chain function calls, making data transformations read from left to right, top to bottom.
let processData rawData =
rawData
|> parseJson
|> transformRecords
|> filterValidEntries
|> aggregateResults
Expressions over Statements
F# is an expression-oriented language. Prefer constructs that return values.
Example: An if
expression returns a value.
let getStatusMessage isSuccess =
if isSuccess then
"Operation completed successfully."
else
"Operation failed."
Error Handling
Use the Result<'T, 'Error>
type for explicit error handling. Exceptions should be reserved for truly exceptional, unrecoverable situations.
type ServiceResult<'T> =
| Success of 'T
| Failure of string
let tryOpenFile path =
try
let content = System.IO.File.ReadAllText path
Success content
with
| ex -> Failure ex.Message
Comments
Use comments to explain why code is written in a certain way, not what it does. F# supports block comments (
(* ... *)
) and line comments (// ...
).