F# Style Guide

Best practices for writing idiomatic and maintainable F# code.

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


// This function is intentionally simple for demonstration purposes.
let processItem item =
    (*
    This block comment explains a more complex reasoning.
    We are using a workaround here because of a known issue in
    the upstream library version 1.2.3.
    *)
    item * 2