Files
everything-claude-code/rules/fsharp/coding-style.md
2026-05-11 21:43:17 -04:00

2.8 KiB

paths
paths
**/*.fs
**/*.fsx

F# Coding Style

This file extends common/coding-style.md with F#-specific content.

Standards

  • Follow standard F# conventions and leverage the type system for correctness
  • Prefer immutability by default; use mutable only when justified by performance
  • Keep modules focused and cohesive

Types and Models

  • Prefer discriminated unions for domain modeling over class hierarchies
  • Use records for data with named fields
  • Use single-case unions for type-safe wrappers around primitives
  • Avoid classes unless interop or mutable state requires them
type EmailAddress = EmailAddress of string

type OrderStatus =
    | Pending
    | Confirmed of confirmedAt: DateTimeOffset
    | Shipped of trackingNumber: string
    | Cancelled of reason: string

type Order =
    { Id: Guid
      CustomerId: string
      Status: OrderStatus
      Items: OrderItem list }

Immutability

  • Records are immutable by default; use with expressions for updates
  • Prefer list, map, set over mutable collections
  • Avoid ref cells and mutable fields in domain logic
let rename (profile: UserProfile) newName =
    { profile with Name = newName }

Function Style

  • Prefer small, composable functions over large methods
  • Use the pipe operator |> to build readable data pipelines
  • Prefer pattern matching over if/else chains
  • Use Option instead of null; use Result for operations that can fail
let processOrder order =
    order
    |> validateItems
    |> Result.bind calculateTotal
    |> Result.map applyDiscount
    |> Result.mapError OrderError

Async and Error Handling

  • Use task { } for interop with .NET async APIs
  • Use async { } for F#-native async workflows
  • Propagate CancellationToken through public async APIs
  • Prefer Result and railway-oriented programming over exceptions for expected failures
let loadOrderAsync (orderId: Guid) (ct: CancellationToken) =
    task {
        let! order = repository.FindAsync(orderId, ct)
        return
            order
            |> Option.defaultWith (fun () ->
                failwith $"Order {orderId} was not found.")
    }

Formatting

  • Use fantomas for automatic formatting
  • Prefer significant whitespace; avoid unnecessary parentheses
  • Remove unused open declarations

Open Declaration Order

Group open statements into four sections separated by a blank line, each section sorted lexically within itself:

  1. System.*
  2. Microsoft.*
  3. Third-party namespaces
  4. First-party / project namespaces
open System
open System.Collections.Generic
open System.Threading.Tasks

open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging

open FsCheck.Xunit
open Swensen.Unquote

open MyApp.Domain
open MyApp.Infrastructure