npx claudepluginhub nwave-ai/nwave --plugin nwThis skill uses the workspace's default tool permissions.
Cross-references: [fp-principles](./fp-principles.md) | [fp-domain-modeling](./fp-domain-modeling.md) | [pbt-dotnet](./pbt-dotnet.md)
Guides Haskell FP for type-safe domain modeling using sum types, newtypes, smart constructors, monads, applicatives. Setup and patterns for correctness-critical systems.
Applies Scala functional patterns including immutability, higher-order functions, pattern matching, ADTs, monads, for-comprehensions, and composition for type-safe applications.
Writes modern C# code using records, pattern matching, async/await. Optimizes .NET apps, implements enterprise patterns like SOLID, and provides comprehensive testing with xUnit, Moq.
Share bugs, ideas, or general feedback.
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.