Install any skill in seconds. Free to start, no credit card required.
Get Started Free →F# language-specific patterns, Railway-Oriented Programming, and Computation Expressions
.claude/skills/nwave-ai-nw-fp-fsharp/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 20% | 0% |
| case-19 | ✓→✗ | ▼ Worse | 121% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 69% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 67% | 0% |
Cross-references: fp-principles | fp-domain-modeling | pbt-dotnet
bashdotnet new console -lang F# -o OrderService && cd OrderService dotnet new xunit -lang F# -o OrderService.Tests dotnet add OrderService.Tests reference OrderService dotnet add OrderService.Tests package FsCheck.Xunit dotnet test
File order matters: F# compiles files top-to-bottom as listed in .fsproj. Types must be defined before use.
fsharptype PaymentMethod = | CreditCard of cardNumber: string * expiryDate: string | BankTransfer of accountNumber: string | Cash
fsharptype Customer = { CustomerId: CustomerId CustomerName: CustomerName CustomerEmail: EmailAddress } type OrderId = OrderId of int type EmailAddress = EmailAddress of string
Records have structural equality by default. Single-case DUs have small runtime cost (unlike Haskell's zero-cost newtype).
fsharpmodule EmailAddress = let create (rawEmail: string) : Result<EmailAddress, string> = if rawEmail.Contains("@") then Ok (EmailAddress rawEmail) else Error $"Invalid email: {rawEmail}" let value (EmailAddress email) = email
fsharplet processOrder rawOrder = rawOrder |> validateOrder |> Result.bind priceOrder |> Result.bind confirmOrder |> Result.map generateReceipt
Data-last convention: F# functions put primary input last so they compose with |>.
fsharplet placeOrder unvalidatedOrder = unvalidatedOrder |> validateOrder |> Result.bind priceOrder |> Result.bind confirmOrder |> Result.mapError PlaceOrderError.Validation
fsharpopen FsToolkit.ErrorHandling let placeOrder rawOrder = result { let! validated = validateOrder rawOrder let! priced = priceOrder validated return! confirmOrder priced }
Key builders: result { } (error-track) | async { } (async I/O) | task { } (.NET Task interop) | validation { } (accumulate errors, FsToolkit).
F# is impure by default. Purity maintained by architectural convention, not the compiler.
fsharp// Pure domain logic (no I/O, no mutation) module Domain = let calculateDiscount (order: Order) : Discount = if List.length order.OrderLines > 10 then Discount 0.1m else Discount 0.0m // Imperative shell (I/O at edges) module App = let placeOrderHandler (deps: Dependencies) (rawOrder: UnvalidatedOrder) = async { let! result = rawOrder |> Domain.validateOrder deps.CheckProductExists |> Result.bind (Domain.priceOrder deps.GetProductPrice) do! deps.SaveOrder result return result }
fsharp// Ports as function types type FindOrder = OrderId -> Async<Order option> type SaveOrder = Order -> Async<unit> // Adapter: concrete implementation let findOrderInDb (connStr: string) (orderId: OrderId) : Async<Order option> = async { (* database query *) } // Composition root: partially apply dependencies let findOrder = findOrderInDb "Server=localhost;Database=orders"
Dependencies first, primary input last. Partially apply at composition root.
Frameworks: FsCheck (QuickCheck port) | fsharp-hedgehog (integrated shrinking) | Expecto (F#-native) | Unquote (assertions). See pbt-dotnet for detailed PBT patterns.
fsharpopen FsCheck.Xunit [<Property>] let ``validated orders always have positive totals`` (rawOrder: RawOrder) = match validateOrder rawOrder with | Error _ -> true | Ok valid -> orderTotal valid > Money 0m [<Property>] let ``serialization round-trips`` (order: Order) = order |> serialize |> deserialize = Ok order
fsharplet genValidEmail = gen { let! user = Gen.nonEmptyListOf (Gen.elements ['a'..'z']) |> Gen.map (fun cs -> System.String(Array.ofList cs)) let! domain = Gen.nonEmptyListOf (Gen.elements ['a'..'z']) |> Gen.map (fun cs -> System.String(Array.ofList cs)) return EmailAddress $"{user}@{domain}.com" }
fsharptype UnvalidatedOrder = { RawName: string; RawEmail: string; RawLines: string list } type ValidatedOrder = { Name: CustomerName; Email: EmailAddress; Lines: OrderLine list } type PricedOrder = { ValidOrder: ValidatedOrder; Total: Money; Lines: PricedOrderLine list }
Each stage is a distinct type. Pipeline transforms one into the next.
fsharpopen FsToolkit.ErrorHandling let validateCustomer (raw: RawCustomer) = validation { let! name = validateName raw.Name and! email = validateEmail raw.Email and! address = validateAddress raw.Address return { Name = name; Email = email; Address = address } }
Project structure: Domain types/workflows in OrderService.Domain/ | adapters in OrderService.Infrastructure/ | composition root in OrderService.App/. File ordering in .fsproj defines compilation order.
B.fs cannot reference A.fs if A.fs listed after B.fs. Reorder files when adding dependencies.Result<_,_> vs Option<_> generically. Use concrete types or computation expressions.Result.mapError before Result.bind.Other measured skills in the registry, with their headline benchmark lift.