跳到主要内容
知仓学习社ZHICANG

nw-fp-fsharp

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

不碰外部(只输出文字)无严重或高危命中nWave-ai/nWave

它会碰到什么

扫了多少1 个文本文件,7 KB
它会碰到什么不碰外部(只输出文字)
命中总数0 处
命中统计严重 0 · 高 0 · 中 0 · 低 0

这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。

技能内容

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) | [property-based-testing](../nw-property-based-testing/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 [property-based-testing](../nw-property-based-testing/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 }
}

Project structure: Domain types/workflows in OrderService.Domain/ | adapters in OrderService.Infrastructure/ | composition root in OrderService.App/. File ordering in .fsproj defines compilation order.

Maturity and Adoption

  • .NET dependency: Deployment outside .NET (native, WASM) is limited. Tooling improvements lag behind C#.
  • Smaller community: Fewer libraries, tutorials, Stack Overflow answers than C#. Community is helpful but small.
  • File ordering constraint: Top-to-bottom compilation prevents circular dependencies (benefit) but frustrates developers used to free ordering. Refactoring file order is a real cost.
  • Second-class .NET citizen: New .NET features (Blazor, MAUI) often ship C#-first with delayed or incomplete F# support.

Common Pitfalls

  1. File order dependency: Types in B.fs cannot reference A.fs if A.fs listed after B.fs. Reorder files when adding dependencies.
  2. No higher-kinded types: Cannot abstract over Result<_,_> vs Option<_> generically. Use concrete types or computation expressions.
  3. .NET OO pressure: C# interop pushes toward classes. Resist: use modules, records, and DUs as primary modeling tools.
  4. Forgetting Result.mapError: When composing steps with different error types, unify with Result.mapError before Result.bind.

想直接用这个技能?

本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。

同名技能的其他版本

有 2 个不同仓库或目录里都有叫 nw-fp-fsharp 的技能。它们内容并不相同,别混用:

  • nWave-ai/nWave — F# language-specific patterns, Railway-Oriented Programming, and Computation Expressions