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 (// ...).