Skip to content
Development
Skill

/nw-fp-fsharp

F# language-specific patterns, Railway-Oriented Programming, and Computation Expressions

From plugin
nwave
628151 skills34 agents27 commands

Claude Code

$ npx -y skills add nWave-ai/nWave --skill nw-fp-fsharp --agent claude-code

SKILL.md

nw-fp-fsharp.SKILL.md
name: nw-fp-fsharp
agent: nw-functional-software-crafter
description: F# language-specific patterns, Railway-Oriented Programming, and Computation Expressions
user-invocable: false
disable-model-invocation: true

FP in F# -- Functional Software Crafter Skill

Cross-references: [fp-principles](../nw-fp-principles/SKILL.md) | [fp-domain-modeling](../nw-fp-domain-modeling/SKILL.md) | [pbt-dotnet](../nw-pbt-dotnet/SKILL.md)

When to Choose F#

  • Best for: domain modeling on .NET | DDD | railway-oriented programming | pipeline-first design | finance
  • Not ideal for: teams needing higher-kinded types | non-.NET platforms | large existing C# codebases resistant to change

[STARTER] Quick Setup

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.

[STARTER] Type System for Domain Modeling

Choice Types (Discriminated Unions)

type PaymentMethod =
    | CreditCard of cardNumber: string * expiryDate: string
    | BankTransfer of accountNumber: string
    | Cash

Record Types and Domain Wrappers

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).

[STARTER] Validated Construction (Smart Constructors)

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

[INTERMEDIATE] Composition Style

Pipeline Operator (The Defining Feature)

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 `|>`.

Railway-Oriented Programming (Error-Track Pipelines)

let placeOrder unvalidatedOrder =
    unvalidatedOrder
    |> validateOrder
    |> Result.bind priceOrder
    |> Result.bind confirmOrder
    |> Result.mapError PlaceOrderError.Validation

Computation Expressions for Monadic Syntax

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).

[INTERMEDIATE] Effect Management

F# is impure by default. Purity maintained by architectural convention, not the compiler.

Pure Core / Imperative Shell

// 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
    }

[ADVANCED] Hexagonal Architecture via Partial Application

// 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.

[INTERMEDIATE] Testing

**Frameworks**: FsCheck (QuickCheck port) | fsharp-hedgehog (integrated shrinking) | Expecto (F#-native) | Unquote (assertions). See [pbt-dotnet](../nw-pbt-dotnet/SKILL.md) for detailed PBT patterns.

Property Test Example (FsCheck + xUnit)

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

Custom Generator

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"
}

[ADVANCED] Idiomatic Patterns

Document Lifecycle as Separate Types

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.

Collect-All-Errors Validation

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 }
}

**Projec

Read more
@nwave-ai
Ships withnwave

AI agents that guide you from idea to working code, with human judgment at every gate. nWave runs inside Claude Code. It breaks feature delivery into seven waves (discover, diverge, discuss, design, devops, distill, deliver).

628151 skills34 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
628
Star history of nWave-ai/nWave on GitHub, a chart by star-history.com

Other skills on nwave.

nw-ad-critique-dimensions

nw-ad-critique-dimensi…

Review dimensions for acceptance test quality - happy path bias, GWT compliance, business language purity, coverage completeness,…

@nwave-ai@nwave-aiInstall Skill
nw-agent-testing

nw-agent-testing

5-layer testing approach for agent validation including adversarial testing, security validation, and prompt injection resistance

@nwave-ai@nwave-aiInstall Skill

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about nw-fp-fsharp.

Which coding agents can use nw-fp-fsharp?
nw-fp-fsharp installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.