nWave/skills/nw-fp-fsharp/SKILL.md
F# language-specific patterns, Railway-Oriented Programming, and Computation Expressions
npx skillsauth add nwave-ai/nwave nw-fp-fsharpInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cross-references: fp-principles | fp-domain-modeling | pbt-dotnet
dotnet 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.
type PaymentMethod =
| CreditCard of cardNumber: string * expiryDate: string
| BankTransfer of accountNumber: string
| Cash
type 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).
module 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
let 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 |>.
let placeOrder unvalidatedOrder =
unvalidatedOrder
|> validateOrder
|> Result.bind priceOrder
|> Result.bind confirmOrder
|> Result.mapError PlaceOrderError.Validation
open 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.
// 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
}
// 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.
open 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
let 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"
}
type 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.
open 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.testing
Acceptance test creation methodology for the DISTILL wave. Domain knowledge for the acceptance designer agent: port-to-port principle, prior wave reading, wave-decision reconciliation, graceful degradation, and document back-propagation.
development
Cross-agent collaboration protocols, workflow handoff patterns, and commit message formats for TDD/Mikado/refactoring workflows
development
Creates a phased roadmap.json for a feature goal with acceptance criteria and TDD steps. Use when planning implementation steps before execution.
testing
Acceptance test creation methodology for the DISTILL wave. Domain knowledge for the acceptance designer agent: port-to-port principle, prior wave reading, wave-decision reconciliation, graceful degradation, and document back-propagation.