2026-05-11 21:43:17 -04:00

113 lines
2.8 KiB
Markdown

---
paths:
- "**/*.fs"
- "**/*.fsx"
---
# F# Coding Style
> This file extends [common/coding-style.md](../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
```fsharp
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
```fsharp
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
```fsharp
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
```fsharp
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
```fsharp
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
```