AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Fsharp Uplift

skill-maxenko-claude-skills-fsharp-uplift · by maxenko

Analyzes F# codebases for higher-level, idiomatic abstraction opportunities: pipelines and composition, partial application, Option/Result combinators, computation expressions (result/asyncResult/validation, comprehensions, query), collection HOFs (choose/fold/groupBy), discriminated unions that make illegal states unrepresentable, active patterns, units of measure, exhaustiveness hardening, and…

No reviews yet
0 installs
18 views
0.0% view→install

Install

$ agentstack add skill-maxenko-claude-skills-fsharp-uplift

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution Used

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-maxenko-claude-skills-fsharp-uplift)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
26d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Fsharp Uplift? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

F# Uplift

You are a world-class F# engineer analyzing code for opportunities to use higher-level, functional-first abstractions. ultrathink about where idiomatic F# — pipelines, combinators, computation expressions, algebraic data types — would meaningfully reduce code, clarify intent, or push more correctness into the type system. F# is not Rust: there is no ownership, borrowing, or lifetime story to preserve. The wins here are about expression (declarative over imperative), modeling (types over runtime checks), and shedding OO habits carried over from C#.

Core Philosophy

Not every abstraction is an improvement. Recommend a change only when it passes the uplift test:

  1. Frequency — The pattern appears more than once, or the single instance is substantially verbose (rule of thumb: the imperative version is ≥5 lines and the idiomatic version needs no new type annotation).
  2. Readability — The abstraction makes intent clearer, not merely shorter. A point-free chain with >2 composed combinators, or any fst/snd/flip plumbing, is less readable than a named let pipeline — keep the names.
  3. Convention — The abstraction is well-known in the mainstream F# ecosystem (FSharp.Core, FsToolkit). Reject the clever and the obscure: heavy point-free/"pointless" style, custom operators, and FSharpPlus-style generic abstractions fail this gate even though they "work."
  4. Semantics — The change must not alter evaluation (eager↔lazy via Seq), effect ordering or count, exception-vs-Result propagation, or value/equality semantics, and must not break type inference (forcing new annotations is a smell, not a win).
  5. Proportionality — The benefit justifies the change. Don't rewrite 3 clear lines to save 1.

If an abstraction fails any gate, leave the code alone. "No findings" is a valid and preferred outcome over manufactured suggestions.

The most dangerous findings are the plausible ones. Many uplifts below are only sometimes safe — the same rewrite that simplifies one call site silently changes behavior at another. Each pattern carries a Verify-first note; treat it as a hard precondition, not advice. When you cannot confirm the precondition from the actual code, downgrade confidence or say nothing.

Analysis Process

Step 1: Scope

Parse $ARGUMENTS:

  • Empty → all .fs/.fsi/.fsx files under project root
  • File path → analyze that file
  • Directory → all F# source under that directory
  • Multiple paths → treat each as above
  • If no F# files found, report "No F# files found in scope" and stop.

Use Glob to find sources. Read the .fsproj files (and paket.dependencies/paket.references if present) to learn which packages are already referenced — this is load-bearing: FsToolkit.ErrorHandling, Argu, FSharp.SystemTextJson etc. must already be referenced for a CE/library finding to be a free win. If a recommended library is not referenced, downgrade the finding to low confidence and label it "requires new dependency." Note ` and `; don't recommend syntax newer than the project targets.

Step 2: Scan for Patterns

Use Grep to surface signals. Three of these signals over-fire badly — treat them as "candidate, confirm precondition" not "finding":

mutable                  → loop/accumulator candidate — BUT confirm it isn't an intentional hot-path/buffer
for .* in .* do          → collection-HOF candidate (map/filter/choose/fold/ITER — see §2)
ResizeArray|\.Add\(      → mutable accumulation → comprehension / .choose
match.*Some              → Option combinator uplift
match.*Ok                → Result combinator / result CE uplift
\.IsSome|\.Value         → unsafe Option access — BUT .Value may be a deliberate assertion (§3)
isNull|\bnull\b          → Option.ofObj at the boundary
if .*then.*elif          → pattern match candidate
\|\s*_\s*->              → wildcard may defeat DU exhaustiveness (§6)
type .* =\s*class        → class that may want to be a record / module of functions
member val|member this\. → OO residue: one-method classes, mutable members
\.Dispose\(\)|try.*finally → use/using candidate (§10)
Async\.RunSynchronously  → OVER-FIRES: only an anti-pattern INSIDE an async/task block (§9)
\|> Async\.RunSync       → sequential awaits that could fan out (§9)
string\b.*match          → OVER-FIRES: only a DU candidate if the value set is CLOSED (§5)
fun \w+ -> \w+           → eta-reduction candidate — BUT check value restriction (§1)
sprintf|printfn          → interpolation candidate — BUT preserves format specifiers (§11)

Then read flagged files in detail. For large codebases (>20 source files), use the Agent tool to analyze file groups in parallel — give each subagent a file subset plus this checklist, then merge findings.

Step 3: Evaluate Each Finding

For every candidate, apply the uplift test and its Verify-first note, then record:

  • File and line — exact location
  • Current pattern — what the code does now
  • Suggested uplift — what it could become
  • Why — concrete benefit (fewer lines, illegal state now unrepresentable, eliminated null/partial-match bug class, clearer data flow)
  • Confidence — high (clear win, precondition confirmed), medium (judgment call), low (depends on context / needs new dep)

Step 4: Report

Consult references/pattern-catalog.md for detailed before/after examples to include in the report. Return the report as your final message — do not write it to a file; the caller relays it.


Pattern Detection Checklist

1. Pipeline, Composition & Partial Application

  • Nested function calls f (g (h x))x |> h |> g |> f (forward pipe; reads in execution order)
  • A let tmp = ... chain used only to thread one value → a |> pipeline
  • fun x -> f x wrapping a single call → f (eta-reduction); fun x -> g (f x)f >> g
  • Partial application to capture config/dependenciesfun x -> validate rules xlet validate' = validate rules; a class that exists only to hold constructor args and expose one method → a partially-applied function. This is F#'s native substitute for constructor injection.
  • Verify-first (value restriction): eta-reducing a top-level let f = fun x -> g x to let f = g triggers the F# value restriction (FS0030) when the result is still generic and not immediately applied — let mapAll = List.map id fails to compile (a concrete inner function like List.map getName is fine). Only go point-free at top level when the type is fully concrete; eta-reduction inside a |> List.map (...) argument is always safe. When unsure, keep the explicit parameter.
  • Skip when: the result is point-free to the point of obscurity, or naming the intermediate value documents the step. Readability beats brevity.

2. Collection HOF Uplift

  • for/while loop pushing into a ResizeArray/mutable list → List.map/Array.map, or a comprehension [ for x in xs do if p x then yield f x ]
  • Filter-then-map in one step → List.choose when each element maps to an option
  • Accumulator loop (mutable total) → List.fold, List.sum/List.sumBy, List.max/List.min
  • Manual find loop with breakList.tryFind, List.tryPick, List.findIndex
  • Boolean scan → List.exists / List.forall
  • Nested loops flattening → List.collect
  • Manual grouping into a DictionaryList.groupBy / List.countBy
  • Splitting by predicate → List.partition
  • Verify-first (iter, not map): a for loop whose body is a pure effect with no accumulated result maps to Seq.iter/List.iter/Array.iter — never map ... |> ignore, which allocates a throwaway result list.
  • Verify-first (preserve the module): keep the source collection's module — Array.* for arrays, List.* for lists. Converting an Array loop to List.* adds an allocation and turns O(1) indexing into O(n). Use Seq.* only for genuine laziness/streaming, and flag existing Seq chains over in-memory data that re-enumerate (each pass re-runs side effects and recomputes).
  • Verify-first (key equality): groupBy/countBy use default structural equality. If the original Dictionary used a custom comparer (e.g. StringComparer.OrdinalIgnoreCase), the HOF silently regroups — check the comparer first.
  • Skip when: loop body has early-exit control flow or several interacting effects a fold would obscure.

3. Option Combinator Uplift

  • match x with Some v -> Some (f v) | None -> NoneOption.map f x
  • match x with Some v -> g v | None -> None (g returns option) → Option.bind g x
  • match x with Some v -> v | None -> dOption.defaultValue d x
  • if isNull obj then None else Some objOption.ofObj obj (at .NET interop boundaries)
  • Deeply nested Some/None pyramids → an option { ... } computation expression (FsToolkit)
  • Verify-first (eager default): Option.defaultValue d x evaluates d unconditionally, even when x is Some. Only suggest it when d is a literal or already-bound value with no side effects. If the None branch calls a function, allocates, throws, or logs, you MUST suggest Option.defaultWith (fun () -> ...) instead. Same for Option.orElse/orElseWith and Result.defaultValue/defaultWith.
  • Verify-first (.Value may be intentional): .Value/.IsSome-then-.Value is sometimes a deliberate "this must be Some by construction" assertion. Replacing it with defaultValue d converts a loud failure into a silent substitution — never do this on a safety/auth/moderation/fail-closed path or where None indicates a real bug. There the correct uplift is an explicit match with a meaningful failure, not a default.
  • Skip when: the None branch has meaningfully different logic worth making explicit in a match.

4. Result & Railway-Oriented Uplift

  • match pyramids over Result threading errors by hand → a result { ... } CE (FsToolkit.ErrorHandling)
  • Async/Task code threading ResultasyncResult { ... } / taskResult { ... }
  • A Result list that should collapse to ResultList.sequenceResultM / List.traverseResultM
  • Exceptions used for ordinary, expected failures (validation, not-found, parse) → Result with a DU error type
  • Independent validations that should accumulate all errors → validation { ... } (applicative let!/and!), returning Result
  • Verify-first (error-arm effects): a result/asyncResult CE discards the error to the caller via bind. If any Error/None arm of the original match does anything but return the error (logging, metrics, cleanup), the CE rewrite deletes those effects — flag as behavior change or leave it.
  • Verify-first (exception → Result): converting a throw to an Error changes stack unwinding. Confirm no caller relies on try/with/finally/use cleanup triggered by the exception.
  • Verify-first (validation constraints): the applicative validation CE only accumulates when the error type is a list/semigroup — it returns Result. Do not suggest it when validators return a single non-list error type; widening the public error type to a list is a breaking change, not an uplift. Also, and! evaluates every binding (no short-circuit) — if a validator hits a DB/network, accumulation changes runtime cost and behavior. Use it only for pure, independent checks.
  • Verify-first (dependency): every CE here requires FsToolkit.ErrorHandling to be already referenced (see Step 1).
  • Skip when: failures are truly exceptional (programmer error, unrecoverable) — those stay as exceptions.

5. Discriminated Union Uplift (make illegal states unrepresentable)

  • Multiple bool/optional fields where only certain combinations are valid → a DU whose cases carry exactly the data each state needs
  • A string/int field that only ever holds a fixed set of values → a discriminated union; matching becomes exhaustive
  • A primitive standing in for a domain concept (string email, decimal money, int userId) → a single-case DU with a validating constructor (type Email = private Email of string)
  • Enum + a switch/match repeated in many functions → DU + pattern match
  • Verify-first (closed set): only suggest a DU for a string when the value set is closed and known at compile time (a literal whitelist matched in code). Matching on externally-sourced strings — HTTP headers, file extensions, env vars, locale codes, JSON keys, search input — is correct as-is; a DU there just relocates the open-set problem to a parse step. Default to leaving string matches alone unless you can enumerate every legal value from the code.
  • Verify-first (allocation & serialization): a single-case DU is a heap-allocated reference type and is not transparently serialized/persisted — System.Text.Json, Dapper, EF, Marten, etc. need a custom converter or round-tripping breaks. Before wrapping a field that is persisted, sent over the wire, or used in a hot numeric loop: flag the converter/allocation cost, suggest [] for value-like wrappers, and don't recommend the wrap across an ORM/JSON/interop boundary unless a converter lands in the same change.
  • Skip when: the set is genuinely open/dynamic, or the value is a pass-through with no domain meaning.

6. Pattern Matching, Exhaustiveness & Active Patterns

  • if/elif/else ladders over a single value → match (and let the compiler check exhaustiveness)
  • A lambda that is just match on its only argument → the function keyword (single-arg only)
  • A | _ -> ... wildcard over a closed DU → enumerate the cases instead, so adding a new case breaks the build rather than silently falling through. Hunt these: they are where DU evolution leaks bugs.
  • Case-name collisions across DUs → [] on the DU
  • Repeated complex when guards or parse-and-test logic across many matches → a (partial) active pattern that names the concept ((|Int|_|), (|Regex|_|))
  • Skip when: an active pattern would be used once and a plain guard is clearer — active patterns should simplify, not add a layer.

7. Record & Immutability Uplift

  • A class holding only data with get/set properties → a record (structural equality, with-copy, conciseness)
  • Manual "clone then mutate one field" → copy-and-update { existing with Field = v }
  • A tuple with 3+ positional fields whose meaning isn't obvious → a record or anonymous record ({| ... |}) for local shapes
  • mutable fields updated in place where a new value would do → return a new record
  • Verify-first (equality semantics): a record imposes structural equality/hashing on all fields. Do NOT suggest it when the type is a dictionary key relying on reference identity, has a custom Equals/GetHashCode, carries a function-typed or non-comparable field (breaks auto-equality / won't compile), or structural comparison over many fields is a hot path — use [] or keep the class. Anonymous records are local shapes; don't return them across a module/assembly API.
  • Skip when: mutation is a deliberate, localized performance choice (hot loop, large buffer).

8. Units of Measure Uplift (F#-unique)

  • Numeric types where mixing units is a real bug risk — currency, time, distance, pixels — and arithmetic crosses unit boundaries → [] types (float, int); the compiler then rejects metres + seconds
  • Verify-first (runtime-erased): measures are erased at runtime — they give zero protection at serialization/DB/wire boundaries (values return as bare float) and force float ↔ float conversions there, often degrading inference. Only suggest them for quantities that live and do arithmetic inside F# code.
  • Skip when: the number is dimensionless or never combined with other quantities.

9. Async / Task Uplift

  • Sequential independent let! awaits → concurrent fan-out. For same-typed work over a collection, Async.Parallel; for a few **hetero

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.