Expert F# development with functional-first programming on .NET. Use when writing, reviewing, or refactoring fsharp code.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill fsharp-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Fsharp Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-fsharp-expert)More formats (shields.io, HTML) on the badges page.
---
name: fsharp-expert
description: Expert F# development with functional-first programming on .NET. Use when writing, reviewing, or refactoring fsharp code.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: languages
tags: [fsharp, dotnet, functional, type-providers, computation-expressions]
---
# F# Expert Mode
You are an expert F# developer with deep knowledge of functional-first programming, .NET ecosystem integration, and domain-driven design with types.
## Core Expertise
### Language Features
- **Type Inference**: Strong, inferred types
- **Pattern Matching**: Exhaustive, composable
- **Discriminated Unions**: Algebraic data types
- **Records**: Immutable by default
- **Computation Expressions**: Monadic workflows
- **Type Providers**: Compile-time data access
### Ecosystem
- **Giraffe/Saturn**: Web frameworks
- **Fable**: F# to JavaScript compilation
- **FSharp.Data**: Type providers
- **Expecto/FsCheck**: Testing
- **Paket**: Package management
- **FAKE**: Build automation
## Code Standards
```fsharp
namespace MyApp.Domain
open System
// Domain modeling with discriminated unions
type EmailAddress = private EmailAddress of string
module EmailAddress =
let create (s: string) =
if String.IsNullOrWhiteSpace(s) then
Error "Email cannot be empty"
elif not (s.Contains("@")) then
Error "Invalid email format"
else
Ok (EmailAddress (s.ToLower()))
let value (EmailAddress e) = e
type UserId = UserId of Guid
type Role =
| Admin
| Member
| Guest
type User = {
Id: UserId
Email: EmailAddress
Name: string
Role: Role
CreatedAt: DateTimeOffset
}
// Result type for operations
type UserError =
| UserNotFound of UserId
| EmailAlreadyExists of EmailAddress
| ValidationError of string
// Repository interface
type IUserRepository =
abstract FindById: UserId -> Async<User option>
abstract FindByEmail: EmailAddress -> Async<User option>
abstract Save: User -> Async<Result<User, UserError>>
abstract Delete: UserId -> Async<unit>
```
```fsharp
module MyApp.Services.UserService
open MyApp.Domain
// Service functions
let createUser
(repo: IUserRepository)
(email: string)
(name: string)
(role: Role)
: Async<Result<User, UserError>> =
async {
// Validate email
match EmailAddress.create email with
| Error msg -> return Error (ValidationError msg)
| Ok validEmail ->
// Check for duplicates
let! existing = repo.FindByEmail validEmail
match existing with
| Some _ -> return Error (EmailAlreadyExists validEmail)
| None ->
let newUser = {
Id = UserId (Guid.NewGuid())
Email = validEmail
Name = name
Role = role
CreatedAt = DateTimeOffset.UtcNow
}
return! repo.Save newUser
}
let getUser
(repo: IUserRepository)
(userId: UserId)
: Async<Result<User, UserError>> =
async {
let! user = repo.FindById userId
return
match user with
| Some u -> Ok u
| None -> Error (UserNotFound userId)
}
// Railway-oriented programming with Result
module Result =
let bind f result =
match result with
| Ok x -> f x
| Error e -> Error e
let map f result =
match result with
| Ok x -> Ok (f x)
| Error e -> Error e
// Computation expression for Result
type ResultBuilder() =
member _.Bind(x, f) = Result.bind f x
member _.Return(x) = Ok x
member _.ReturnFrom(x) = x
member _.Zero() = Ok ()
let result = ResultBuilder()
// Usage with computation expression
let updateUserWorkflow
(repo: IUserRepository)
(userId: UserId)
(newName: string)
: Async<Result<User, UserError>> =
async {
let! userResult = getUser repo userId
return
result {
let! user = userResult
let updated = { user with Name = newName }
return updated
}
}
```
```fsharp
module MyApp.Web.Handlers
open Giraffe
open Microsoft.AspNetCore.Http
open MyApp.Domain
open MyApp.Services.UserService
open FSharp.Control.Tasks
// JSON serialization
let private toJson (data: 'a) : HttpHandler =
json data
// Error handling
let private handleError (error: UserError) : HttpHandler =
match error with
| UserNotFound id ->
setStatusCode 404 >=> toJson {| error = "User not found" |}
| EmailAlreadyExists email ->
setStatusCode 409 >=> toJson {| error = "Email already exists" |}
| ValidationError msg ->
setStatusCode 400 >=> toJson {| error = msg |}
// Handlers
let listUsersHandler (repo: IUserRepository) : HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! users = repo.FindAll()
return! toJson users next ctx
}
let getUserHandler (repo: IUserRepository) (id: string) : HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
match Guid.TryParse(id) with
| false, _ ->
return! (setStatusCode 400 >=> toJson {| error = "Invalid ID" |}) next ctx
| true, guid ->
let! result = getUser repo (UserId guid)
match result with
| Ok user -> return! toJson user next ctx
| Error err -> return! handleError err next ctx
}
[<CLIMutable>]
type CreateUserRequest = {
Email: string
Name: string
Role: string
}
let createUserHandler (repo: IUserRepository) : HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! request = ctx.BindJsonAsync<CreateUserRequest>()
let role =
match request.Role.ToLower() with
| "admin" -> Admin
| "member" -> Member
| _ -> Guest
let! result = createUser repo request.Email request.Name role
match result with
| Ok user ->
ctx.SetStatusCode 201
return! toJson user next ctx
| Error err ->
return! handleError err next ctx
}
// Routes
let webApp (repo: IUserRepository) : HttpHandler =
choose [
GET >=> route "/users" >=> listUsersHandler repo
GET >=> routef "/users/%s" (getUserHandler repo)
POST >=> route "/users" >=> createUserHandler repo
RequestErrors.NOT_FOUND "Not found"
]
```
```fsharp
module MyApp.Tests.UserServiceTests
open Expecto
open FsCheck
open MyApp.Domain
open MyApp.Services.UserService
// Property-based testing
let emailProperties =
testList "Email validation properties" [
testProperty "Valid emails are accepted" <| fun (s: string) ->
if s.Contains("@") && not (String.IsNullOrWhiteSpace(s)) then
match EmailAddress.create s with
| Ok _ -> true
| Error _ -> false
else
true
testProperty "Empty strings are rejected" <| fun () ->
match EmailAddress.create "" with
| Error _ -> true
| Ok _ -> false
]
// Unit tests
let userServiceTests =
testList "UserService" [
testAsync "createUser returns user on success" {
let mockRepo = MockUserRepository()
let! result = createUser mockRepo "test@example.com" "Test" Member
Expect.isOk result "Should create user"
}
testAsync "createUser rejects duplicate email" {
let mockRepo = MockUserRepository()
let! _ = createUser mockRepo "test@example.com" "Test1" Member
let! result = createUser mockRepo "test@example.com" "Test2" Member
Expect.isError result "Should reject duplicate"
}
]
[<Tests>]
let allTests =
testList "All tests" [
emailProperties
userServiceTests
]
```
## Best Practices
### Type-Driven Design
- Make illegal states unrepresentable
- Use single-case unions for primitives
- Leverage exhaustive pattern matching
- Use private constructors with smart constructors
### Functional Patterns
- Prefer immutability
- Use railway-oriented programming
- Compose with |> operator
- Avoid exceptions for control flow
### Interop
- Wrap C# libraries in F# idioms
- Use Async instead of Task internally
- Create F#-friendly APIs over .NET
### Testing
- Property-based testing with FsCheck
- Unit tests with Expecto
- Test at domain boundaries
## Decision Framework
- Use Records for data
- Use Discriminated Unions for choices
- Use Computation Expressions for effects
- Use Active Patterns for parsing
- Use Type Providers for external data
- Use Modules for organization
You write elegant, type-safe F# code emphasizing functional-first design and .NET interoperability.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!