# Deployment and DevOps Standards for F#
This document outlines coding standards and best practices for deployment and DevOps specific to F#. It aims to guide developers in building, deploying, and operating F# applications efficiently and reliably, while leveraging the advantages of the F# language and ecosystem.
## 1. Build Processes and CI/CD
### 1.1. Standard: Use .NET SDK-based Builds
**Do This:** Use the .NET SDK's build system directly or through a build automation tool.
**Don't Do This:** Rely on custom or outdated build scripts.
**Why:** The .NET SDK offers a consistent and well-supported build process. Utilizing it ensures compatibility, simplifies dependencies, and provides built-in support for features like NuGet package management.
**Example:**
"""fsharp
// build.fsx (FAKE build script)
#r "paket: groupref FAKE remoting"
open Fake.Core
open Fake.DotNet
open Fake.IO
open Fake.IO.FileSystem
open Fake.Paket
Target.create "Restore" (fun _ ->
Paket.Restore ()
)
Target.create "Build" (fun _ ->
DotNet.build (fun p ->
{ p with
Configuration = BuildConfiguration.Release
Framework = DotNet.NET.TargetFramework.net60
}
) "src/YourProject.fsproj"
)
Target.create "Test" (fun _ ->
DotNet.test (fun p ->
{ p with
Configuration = BuildConfiguration.Release }
) "tests/YourProject.Tests.fsproj"
)
Target.create "Publish" (fun _ ->
DotNet.publish (fun p ->
{ p with
Configuration = BuildConfiguration.Release
Framework = DotNet.NET.TargetFramework.net60
Output = "output" }
) "src/YourProject.fsproj"
)
Target.create "Default" (fun _ ->
Fake.Core.BuildServer.Build
[ "Restore"
"Build"
"Test"
"Publish" ]
)
RunTargetOrDefault "Default"
"""
**Anti-Pattern:** Using ad-hoc shell scripts for building, leading to inconsistencies between environments.
### 1.2. Standard: Implement Comprehensive CI/CD Pipelines
**Do This:** Use a CI/CD platform (e.g., Azure DevOps, GitHub Actions, GitLab CI) with automated builds, tests, and deployments.
**Don't Do This:** Manual builds and deployments.
**Why:** Automating the build and deployment process streamlines development, reduces errors, and enables faster and more frequent releases.
**Example (GitHub Actions):**
"""yaml
# .github/workflows/dotnet.yml
name: .NET Build and Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release
- name: Test
run: dotnet test --configuration Release --no-restore --verbosity normal
- name: Publish
run: dotnet publish src/YourProject.fsproj -c Release -o out
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: package
path: out
"""
**Anti-Pattern:** Infrequent integrations and manual deployment processes lead to "integration hell" and slow release cycles.
### 1.3: Standard: Embrace Immutable Infrastructure
**Do This:** Deploy applications as immutable artifacts (e.g., Docker images, Azure ARM templates).
**Don't Do This:** Modifying servers in-place.
**Why:** Immutable infrastructure ensures consistency between environments, reduces configuration drift, and simplifies rollbacks.
**Example (Dockerfile):**
"""dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["src/YourProject/YourProject.fsproj", "src/YourProject/"]
RUN dotnet restore "src/YourProject/YourProject.fsproj"
COPY . .
WORKDIR "/src/src/YourProject"
RUN dotnet build "YourProject.fsproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "YourProject.fsproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "YourProject.dll"]
"""
**Why This Matters for F#**: Immutable infrastructure is particularly useful with F# because compiling F# code produces self-contained applications that are easy to package into Docker containers. This simplifies deployment and ensures consistency across environments.
### 1.4. Standard: Implement Infrastructure as Code (IaC)
**Do This:** Define your infrastructure declaratively using tools like Azure Resource Manager (ARM) templates, Terraform, or Pulumi directly within your solutions.
**Don't Do This:** Manually provisioning or configuring infrastructure.
**Why:** IaC provides version control for infrastructure, simplifies provisioning, ensures repeatability, facilitates disaster recovery, and enables self-service deployment.
**Example (Terraform):**
"""terraform
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "West Europe"
}
resource "azurerm_app_service_plan" "example" {
name = "example-appserviceplan"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
sku {
tier = "Standard"
size = "S1"
}
}
resource "azurerm_app_service" "example" {
name = "example-appservice"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
site_config {
dotnet_framework_version = "v6.0"
}
app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = ""
}
}
"""
**Anti-Pattern:** Manually creating resources in the cloud console.
## 2. Production Considerations
### 2.1. Standard: Implement Centralized Logging
**Do This:** Use a centralized logging system (e.g., Azure Monitor, ELK stack, Seq) to collect and analyze logs from all application instances. Structure your logs for easy analysis (e.g., use JSON).
**Don't Do This:** Rely on local log files or console output.
**Why:** Centralized logging provides visibility into application behavior, simplifies troubleshooting, and enables proactive monitoring.
**Example (Logging with Serilog and structured data):**
"""fsharp
open Serilog
open Serilog.Events
// Configure Serilog
let logger =
LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341") // Replace with your Seq instance
.CreateLogger()
// Use the logger
let ProcessOrder orderId amount =
try
logger.Information("Processing order {OrderId} for amount {Amount}", orderId, amount)
// Simulate processing
if amount > 1000 then raise (System.Exception("Order amount exceeds limit"))
logger.Information("Order {OrderId} processed successfully", orderId)
true // Success
with
| ex ->
logger.Error(ex, "Failed to process order {OrderId}", orderId)
false // Failure
// Example usage
let orderResult = ProcessOrder 123 500
let orderResult2 = ProcessOrder 456 1500 //This will cause an error logged.
"""
**Anti-Pattern:** Scattered log files making it difficult to correlate events and diagnose issues.
### 2.2. Standard: Implement Health Checks
**Do This:** Expose health check endpoints (e.g., "/healthz") that report the application's status. Integrate these endpoints with load balancers and monitoring systems. Consider using a dedicated health check library.
**Don't Do This:** Lack of health checks.
**Why:** Health checks allow automated systems to detect and respond to application failures, ensuring high availability.
**Example (Health check in ASP.NET Core with F#):**
"""fsharp
// Program.fs
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Hosting
[]
let main args =
let builder = WebApplication.CreateBuilder(args)
// Add services to the container.
builder.Services.AddHealthChecks() |> ignore // Add health checks
let app = builder.Build()
// Configure the HTTP request pipeline.
if app.Environment.IsDevelopment() then
app.UseDeveloperExceptionPage() |> ignore
app.UseRouting() |> ignore
// Configure health check endpoint
app.MapHealthChecks("/healthz") |> ignore
app.Run()
0
"""
**Anti-Pattern:** Ignoring health checks as part of deployment making it impossible to properly monitor and recover from system failures.
### 2.3. Standard: Implement Robust Error Handling and Circuit Breakers
**Do This:** Use F#'s powerful pattern matching and "Result" type to handle errors gracefully. Wrap external service calls with circuit breakers to prevent cascading failures.
**Don't Do This:** Ignoring exceptions or relying solely on try/catch blocks without providing specific handling.
**Why:** Ensures resilience by preventing faults from propagating through the system. F#'s features are particularly well-suited for handling error conditions explicitly.
**Example (Error Handling with "Result" and a simple Circuit Breaker):**
"""fsharp
open System
open System.Threading.Tasks
type Result<'T, 'Error> =
| Ok of 'T
| Error of 'Error
let tryGetValue (key: string) : Task> =
task {
try
// Simulate fetching a value (e.g., from a database)
if key = "validKey" then
do! Task.Delay(100) // Simulate delay
return Ok "TheValue"
else
do! Task.Delay(100)
return Error $"Key '{key}' not found."
with
| ex -> return Error ex.Message
}
type CircuitBreakerState =
| Closed
| Open
| HalfOpen
type CircuitBreaker(failureThreshold: int, resetTimeout: TimeSpan) =
let mutable currentState = Closed
let mutable failureCount = 0
let mutable lastFailureTime = DateTime.MinValue
let stateLock = obj()
member this.ExecuteAsync<'T> (operation: unit -> Task<'T>) : Task<'T> =
task {
lock stateLock (fun () ->
match currentState with
| Closed ->
try
let! result = operation()
failureCount <- 0 // Reset counter on success
return result
with
| ex ->
failureCount <- failureCount + 1
lastFailureTime <- DateTime.UtcNow
if failureCount >= failureThreshold then
currentState <- Open
printfn "Circuit Breaker Opened"
raise ex // Re-throw the exception
| Open ->
if DateTime.UtcNow - lastFailureTime > resetTimeout then
currentState <- HalfOpen
printfn "Circuit Breaker Half-Opened"
try
let! result = operation()
failureCount <- 0
currentState <- Closed
printfn "Circuit Breaker Closed"
return result
with
| ex ->
lastFailureTime <- DateTime.UtcNow
printfn "Circuit Breaker Remains Open"
raise ex
else
printfn "Circuit Breaker is Open - returning cached result or throwing exception"
raise (Exception("Circuit Breaker Open")) // replace this!
| HalfOpen ->
try
let! result = operation()
failureCount <- 0
currentState <- Closed
printfn "Circuit Breaker Closed"
return result
with
| ex ->
lastFailureTime <- DateTime.UtcNow
currentState <- Open
printfn "Circuit Breaker Re-Opened"
raise ex
) |> ignore
failwith "Unreachable Code" //Force Compiler to verify "return" statements
}
// Usage
let circuitBreaker = CircuitBreaker(3, TimeSpan.FromSeconds(5.0))
let wrappedGetValue (key: string) : Task> =
circuitBreaker.ExecuteAsync(fun () -> tryGetValue key)
[]
let main argv =
task {
let! result1 = wrappedGetValue "validKey"
match result1 with
| Ok value ->
printfn "Value: %s" value
| Error error ->
printfn "Error: %s" error
//Simulate multiple failures
for i in 1..5 do
try
let! result = wrappedGetValue $"invalidKey_{i}"
match result with
| Ok value -> printfn "Value: %s" value
| Error error -> printfn "Error: %s" error
with
| ex -> printfn $"Exception: {ex.Message}"
do! Task.Delay(TimeSpan.FromSeconds(6.0))
let! result6 = wrappedGetValue "validKey"
match result6 with
| Ok value -> printfn "Value (after timeout): %s" value
| Error error ->
printfn "Error: %s" error
} |> Async.AwaitTask |> ignore
0
"""
**Anti-Pattern:** Uncaught exceptions bringing down the entire application. Services becoming unavailable due to overloaded dependent services.
### 2.4. Standard: Implement Application Insights and Monitoring
**Do This:** Instrument your F# applications with Application Insights (or similar APM tools) to collect performance metrics, track dependencies, and monitor exceptions. Create custom metrics specific to your application domain.
**Don't Do This:** Lack of performance monitoring and relying on anecdotal evidence to diagnose performance bottlenecks.
**Why:** Provides actionable insights into application performance, helps identify bottlenecks, and allows for proactive optimization.
**Example (Sending telemetry with Application Insights
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for F# This document outlines the component design standards for F# development, aimed at creating reusable, maintainable, and high-performance components. These standards are based on best practices, the latest F# version features, and insights from the F# community. ## 1. General Principles * **Do This**: Embrace functional principles: immutability, composition, and pure functions. * **Why**: This fosters easier reasoning about code, simplifies testing, and reduces the likelihood of side effects which can lead to bugs. Components built on these principles promotes reusability and scalability. * **Don't Do This**: Rely heavily on mutable state within core component logic. Avoid complex object hierarchies where possible. * **Why**: Mutable state makes reasoning about code difficult. Object hierarchies can lead to tight coupling and reduce reusability. * **Do This**: Design for testability from the start. Make components easily injectable and mockable. * **Why**: Testing is critical for maintainability. Components that are designed with testability in mind are easier to verify and refactor. * **Don't Do This**: Create components with tight dependencies on external systems or libraries that are difficult to mock or replace. * **Why**: This makes testing more difficult and limits the component's reusability. * **Do This**: Prioritize clarity and expressiveness in your code. Use meaningful names and clear comments. * **Why**: Readable code is maintainable code. Expressive code reduces cognitive load and makes it easier for others to understand and contribute. * **Don't Do This**: Sacrifice readability for the sake of brevity. Opaque or overly clever code is difficult to maintain. ## 2. Abstraction and Interfaces * **Do This**: Use interfaces (abstract classes or F# interfaces) to define contracts between components for broader interoperability and testability. * **Why**: Interfaces decouple components, allowing for independent development, testing, and evolution. They enforce a standard contract for interactions. * **Example**: """fsharp // Defining an interface type IDataService = abstract member GetData : string -> Result<string, string> // Implementation of the interface type DataService() = interface IDataService with member this.GetData (key: string) = try // Simulate fetching data (replace with real implementation) if key = "validKey" then Ok "Data fetched successfully" else Error "Key not found" with | ex -> Error ex.Message // Using the interface let processData (service: IDataService) (key: string) = match service.GetData key with | Ok data -> printfn "Data: %s" data | Error error -> printfn "Error: %s" error // Example usage let service = DataService() :> IDataService // explicit upcast processData service "validKey" processData service "invalidKey" """ * **Don't Do This**: Expose concrete implementation details through interfaces. * **Why**: This defeats the purpose of abstraction and can lead to tight coupling. * **Do This**: Prefer discriminated unions for representing data across component boundaries when appropriate, especially when defining a finite set of possibilities. * **Why**: Discriminated unions provide type safety and exhaustiveness checks. They are highly suitable for representing different states or categories of data. * **Example**: """fsharp type Result<'T, 'Error> = | Success of 'T | Failure of 'Error // Using the interface let processResult (result: Result<string, string>) = match result with | Success data -> printfn "Data: %s" data | Failure error -> printfn "Error: %s" error // Implementing a function using the discriminated union let fetchData (key: string) : Result<string, string> = if key = "validKey" then Success "Data fetched successfully" else Failure "Key not found" // Example usage processResult (fetchData "validKey") processResult (fetchData "invalidKey") """ * **Don't Do This**: Use inheritance excessively. Prefer composition over inheritance. * **Why**: Inheritance can lead to fragile base class problems and tight coupling. Composition provides greater flexibility and promotes code reuse. ## 3. Error Handling * **Do This**: Use "Result<'T, 'Error>" to handle errors explicitly. Represent success and failure paths clearly. * **Why**: Explicit error handling improves code clarity and prevents unexpected exceptions and null reference exceptions. Forces callers to handle potential failure scenarios. * **Example**: """fsharp let divide x y : Result<float, string> = if y = 0.0 then Error "Cannot divide by zero" else Ok (x / y) match divide 10.0 2.0 with | Ok result -> printfn "Result: %f" result | Error message -> printfn "Error: %s" message """ * **Don't Do This**: Rely solely on exceptions for control flow. Exceptions should be reserved for exceptional circumstances. * **Why**: Excessive use of exceptions for control flow is inefficient and makes code harder to reason about. Avoid throwing exceptions to indicate normal, expected outcomes. * **Do This**: Log errors and warnings with sufficient context, including timestamps, user information (if available), and relevant data. Use structured logging when possible. * **Why**: Good logging is crucial for debugging and monitoring the health of components. * **Don't Do This**: Log sensitive information (passwords, API keys, etc.) or personally identifiable information (PII). * **Why**: Security and privacy. * **Do This**: Consider using custom exception types for failures that need special contextual information. * **Why**: Allows catching specific errors to handle them appropriately. * **Don't Do This**: Rethrow exceptions without adding context, masking the true place of the exception. ## 4. Immutability and Pure Functions * **Do This**: Make data immutable by default using "let" bindings and immutable data structures (records, lists, sets, maps). * **Why**: Immutability simplifies reasoning about code, prevents unintended side effects, and improves concurrency safety. * **Example**: """fsharp type Person = { Name: string; Age: int } let createPerson name age = { Name = name; Age = age } let updateAge person newAge = { person with Age = newAge } let person1 = createPerson "Alice" 30 let person2 = updateAge person1 31 // Returns a new Person record """ * **Don't Do This**: Use mutable variables ("mutable" keyword) unless absolutely necessary. If you must use mutation, isolate it within a small, well-defined scope. * **Why**: Mutable state introduces complexity and potential for errors. * **Do This**: Write pure functions whenever possible. A pure function always returns the same output for the same input and has no side effects. * **Why**: Pure functions are easy to test, compose, and reason about. They promote code reuse and modularity. * **Example**: """fsharp let add x y = x + y // Pure function let result = add 5 3 // Always returns 8 """ * **Don't Do This**: Create functions that modify external state or rely on external state that can change unexpectedly. * **Why**: These functions are harder to test and reason about. ## 5. Asynchronous Programming * **Do This**: Use "async" workflows for asynchronous operations involving I/O or long-running computations. * **Why**: Asynchronous programming prevents blocking the main thread, improving responsiveness and scalability. * **Example**: """fsharp open System.Net.Http let fetchContent url = async { use client = new HttpClient() let! response = client.GetStringAsync(url) |> Async.AwaitTask return response } // Execute the asynchronous operation let task = fetchContent "https://example.com" let content = Async.RunSynchronously task printfn "%s" content """ * **Don't Do This**: Block the main thread with synchronous I/O operations. * **Why**: This can lead to unresponsiveness and performance problems. * **Do This**: Handle exceptions within "async" workflows using "try...with". * **Why**: To prevent unhandled exceptions from crashing the application. * **Don't Do This**: Use "Async.RunSynchronously" in UI threads. It leads to deadlocks. * **Do This**: Use "Task<'T>" and ".NET" concurrent collections when interacting with ".NET" libraries. * **Why**: Increases interoperability with ".NET" ecosystem allowing to use F# in mixed ".NET"/F# solutions. ## 6. Naming Conventions * **Do This**: Use descriptive and meaningful names for types, functions, and variables. * **Why**: Good naming improves code readability and understandability. * **Don't Do This**: Use abbreviations or cryptic names that are difficult to understand. * **Do This**: Follow these conventions: * Types: PascalCase (e.g., "DataService", "Person") * Functions: camelCase (e.g., "getData", "calculateTotal") * Variables: camelCase (e.g., "name", "age") * Parameters: camelCase (e.g., "input", "value") * Modules: PascalCase (e.g., "DataModule", "MathUtils") * **Don't Do This**: Deviate from established naming conventions. * **Do This**: Use plural names for collections (e.g., "customers", "products"). * **Why**: This clearly communicates that the variable represents a collection. ## 7. Dependency Injection * **Do This**: Use dependency injection to provide components with their dependencies. * **Why**: Dependency injection promotes loose coupling, making components more testable and reusable. * **Don't Do This**: Hardcode dependencies within components. * **Do This**: Use constructor injection to provide dependencies. * **Why**: Constructor injection makes dependencies explicit and ensures that components receive their required dependencies. * **Example:** """fsharp type OrderProcessor (dataService: IDataService, emailService: IEmailService) = member this.ProcessOrder (orderId: string) = match dataService.GetData orderId with | Ok orderData -> emailService.SendConfirmationEmail(orderData.CustomerEmail, orderId) Ok () | Error error -> Error error """ * **Don't Do This**: Expose setter properties for dependencies (property injection) unless there is a specific reason to do so. Constructor injection is preferable. * **Do This**: Favor interfaces for dependencies to make mocking easier and allow for multiple implementations. ## 8. Performance * **Do This**: Use immutable data structures efficiently. When modifying collections, consider using persistent data structures or specialized mutable collections when performance is critical. * **Why**: Immutability is generally good but might has performance penalties in specific scenarios. * **Don't Do This**: Create excessive copies of large data structures unnecessarily. * **Do This**: Measure and profile code to identify performance bottlenecks. Use performance testing tools like BenchmarkDotNet. * **Why**: To identify and optimize performance-critical sections of code. * **Don't Do This**: Make premature optimizations without measuring performance. * **Do This**: Use tail recursion optimization to prevent stack overflows in recursive functions. * **Why**: Improves performance and prevents stack overflow errors. * **Example**: """fsharp // Tail-recursive function let rec factorial n acc = if n = 0 then acc else factorial (n - 1) (n * acc) let result = factorial 5 1 // Initial accumulator value is 1 // Non-tail-recursive function (will cause StackOverflowException for large n) let rec factorialNonTailRecursive n = if n = 0 then 1 else n * factorialNonTailRecursive (n - 1) """ * **Don't Do This:** Implement recursive functions without taking tail recursion into account. ## 9. Security * **Do This**: Parameterize queries and commands to prevent SQL injection. * **Why**: Prevents malicious code injection into database queries. * **Don't Do This**: Construct SQL queries using string concatenation with user-supplied input. * **Do This**: Sanitize user input to prevent cross-site scripting (XSS) attacks. * **Why**: Prevents malicious scripts from being injected into web pages. * **Don't Do This**: Display unsanitized user input directly on web pages. * **Do This**: Use appropriate authentication and authorization mechanisms to protect sensitive data and functionality. * **Why**: Ensures that only authorized users can access specific resources. * **Don't Do This**: Store passwords in plain text. Use strong hashing algorithms with salting. * **Do This**: Avoid using deprecated or vulnerable libraries. Keep dependencies up to date. * **Why**: To inherit the best security patches for the known vulnerabilities. * **Don't Do This**: Blindly trust external dependencies. Review security advisories and assess risks. ## 10. Documentation * **Do This**: Provide clear and concise documentation for all public types, functions, and modules. * **Why**: Documentation makes components easier to understand and use. * **Don't Do This**: Omit documentation or provide inadequate documentation. * **Do This**: Use F# signature files (".fsi") to define public interfaces and document them. * **Why**: Signature files provide a clear contract for the public API of a component. * **Don't Do This**: Expose implementation details in signature files. * **Do This**: Include examples of how to use components in the documentation. * **Why**: Examples make it easier for users to get started with the component. * **Don't Do This**: Provide outdated or incorrect examples. * **Do This**: Document any known limitations or potential issues with components. * **Why**: Transparency builds trust and helps users avoid common pitfalls. ## 11. Tooling * **Do This**: Use a code formatter (e.g., Fantomas) to ensure consistent code formatting. * **Why**: Consistent formatting improves code readability and reduces visual noise. * **Don't Do This**: Rely on manual formatting or inconsistent formatting styles. * **Do This**: Use a code analyzer (e.g., FSharpLint) to identify potential issues and enforce coding standards. * **Why**: Code analyzers help catch errors and enforce best practices early in the development process. * **Don't Do This**: Ignore warnings or errors reported by code analyzers. * **Do This**: Integrate testing into the build process. * **Why**: Ensures that tests are run automatically whenever code is changed. * **Don't Do This**: Skip testing or rely on manual testing. * **Do This**: Familiarize yourself with the F# compiler options and use them to optimize code generation. ## 12. Specific F# Patterns and Anti-Patterns * **Do This**: Use computation expressions for complex control flow, asynchronous programming, or working with monads. * **Why**: Computation expressions provide a concise and readable way to express complex logic. * **Example**: """fsharp type MaybeBuilder() = member this.Bind(x, f) = match x with | Some v -> f v | None -> None member this.Return(x) = Some x member this.ReturnFrom(x) = x member this.Zero() = None let maybe = MaybeBuilder() let divide x y = if y = 0 then None else Some (x / y) let result = maybe { let! a = divide 10 2 let! b = divide a 5 return b } printfn "%A" result """ * **Don't Do This**: Overuse computation expressions for simple operations. * **Do This**: Utilize pattern matching extensively for data decomposition and control flow. * **Why**: Pattern matching provides a powerful and expressive way to work with discriminated unions and other data types. * **Don't Do This**: Use nested "if...then...else" statements excessively. Pattern matching is often a better alternative. * **Do This**: When using "Seq.fold" or "List.fold", pay attention to the order of arguments. * **Why**: "Seq.fold"'s accumulator receives the current state as its first argument, while the current element is the second argument. * **Don't Do This**: Confuse arguments leading to incorrect state and calculation. This comprehensive document provides a strong foundation for building robust, maintainable, and scalable F# components. Adhering to these standards improves code quality, reduces errors, and promotes collaboration within development teams. By following these guidelines, development teams ensures its resulting code base is built to industry standards.
# Core Architecture Standards for F# This document outlines the core architectural standards for F# development. It aims to provide guidelines for structuring projects, organizing code, and applying architectural patterns to ensure maintainability, scalability, and performance. These standards are designed for modern F# development and incorporate recent language features and ecosystem best practices. ## 1. Fundamental Architectural Patterns This section outlines the fundamental architectural patterns recommended for F# projects. ### 1.1 Functional Core, Imperative Shell **Rationale:** This pattern isolates business logic into a purely functional core, enabling testability and composability. The imperative "shell" handles side effects like I/O, database interactions, and UI updates. **Do This:** * Encapsulate all business logic within F# modules and functions that are free from side effects. * Use discriminated unions and record types to model domain data. * Orchestrate the functional core from an imperative shell that handles external interactions. **Don't Do This:** * Mix side-effecting code directly within core business logic. * Rely on mutable state within the functional core. * Pass external dependencies directly into the core. Use functions instead. **Example:** """fsharp // Functional Core module OrderProcessing = type Order = { OrderId : int CustomerId : int Items : string list TotalAmount : decimal } type OrderError = | InvalidOrder | InsufficientStock let validateOrder (order: Order) : Result<Order, OrderError> = if order.TotalAmount > 0m && order.Items.Length > 0 then Ok order else Error InvalidOrder let calculateTotal (order: Order) : decimal = order.Items |> List.length |> decimal * 10.0m // Imperative Shell module OrderService = open OrderProcessing let processOrder (order: Order) = match validateOrder order with | Ok validOrder -> // Simulate database interaction printfn "Saving order to database: %A" validOrder // Simulate stock update printfn "Updating stock levels for order: %A" validOrder Ok validOrder | Error error -> printfn "Order processing failed: %A" error Error error let createOrder (orderId: int) (customerId: int) (items: string list) : Order = { OrderId = orderId; CustomerId = customerId; Items = items; TotalAmount = calculateTotal { OrderId = orderId; CustomerId = customerId; Items = items; TotalAmount = 0.0m } } // Usage (in a separate application layer or main program) let order = OrderService.createOrder 123 456 ["Item1"; "Item2"] let result = OrderService.processOrder order match result with | Ok _ -> printfn "Order processed successfully" | Error _ -> printfn "Order processing failed" """ **Anti-Patterns:** * **Tight Coupling:** Directly instantiating external dependencies (e.g., database connections) within the functional core. Use dependency injection techniques (see below). * **Impure Functions:** Functions that rely on or modify global state. ### 1.2 Onion Architecture **Rationale:** Onion Architecture promotes loose coupling and testability by placing domain logic at the core and surrounding it with layers of infrastructure concerns. **Do This:** * Define core domain entities and use cases in an inner layer. * Implement interfaces for infrastructure services (e.g., data access, messaging) in the inner layer. * Create concrete implementations of these interfaces in outer layers. * Use dependency injection to provide implementations to the core. **Don't Do This:** * Allow outer layers to depend directly on inner layers. * Embed infrastructure concerns within core domain logic. **Example:** """fsharp // Core (Domain) Layer module Domain = type Order = { OrderId : int CustomerId : int Items : string list } type IOrderRepository = abstract member GetOrder : int -> Order option abstract member SaveOrder : Order -> unit type OrderService(orderRepository: IOrderRepository) = member this.GetOrder(orderId: int) = orderRepository.GetOrder orderId // Infrastructure Layer module Infrastructure = open Domain type OrderRepository() = interface IOrderRepository with member this.GetOrder (orderId: int) = // Simulate database retrieval if orderId = 123 then Some { OrderId = 123; CustomerId = 456; Items = ["Item1"; "Item2"] } else None member this.SaveOrder (order: Order) = // Simulate saving to database printfn "Saving order to database: %A" order // Application Layer (Composition Root) module Application = open Domain open Infrastructure let orderRepository = OrderRepository() :> IOrderRepository let orderService = OrderService(orderRepository) let getOrder (orderId: int) = orderService.GetOrder orderId // Usage let order = Application.getOrder 123 match order with | Some o -> printfn "Order: %A" o | None -> printfn "Order not found" """ **Anti-Patterns:** * **Layer Violations:** An outer layer directly accessing an inner layer's implementation details. * **God Class:** Centralizing too much logic in a single, massive class. ### 1.3 Microservices Architecture **Rationale:** Decomposing a large application into smaller, independent services promotes scalability, fault isolation, and independent deployments. **Do This:** * Define clear boundaries between microservices based on business capabilities. * Use asynchronous communication (e.g., message queues) for inter-service communication. * Design each service to be independently deployable and scalable. * Implement robust error handling and monitoring for each service. **Don't Do This:** * Create tightly coupled microservices that depend heavily on each other. * Share databases between microservices. * Over-engineer communication protocols. **Example (Simplified):** """fsharp // Order Service module OrderService = open System open System.Threading.Tasks let processOrder (orderId: int) : Task<string> = Task.Run(fun () -> // Simulate order processing printfn "Processing order %d in OrderService" orderId Task.Delay(1000) |> Async.AwaitTask |> Async.RunSynchronously sprintf "Order %d processed successfully" orderId) // Notification Service module NotificationService = open System open System.Threading.Tasks let sendNotification (message: string) : Task<unit> = Task.Run(fun () -> // Simulate sending notification printfn "Sending notification: %s in NotificationService" message Task.Delay(500) |> Async.AwaitTask |> Async.RunSynchronously ()).Ignore() // Orchestration (e.g., using a message queue) module Orchestration = open OrderService open NotificationService let handleOrderRequest (orderId: int) = processOrder orderId |> Async.AwaitTask |> Async.map (fun result -> sendNotification result |> ignore ) |> Async.RunSynchronously // Usage (simulated message queue trigger) Orchestration.handleOrderRequest 456 """ **Anti-Patterns:** * **Distributed Monolith:** Decomposing an application into microservices without proper bounded contexts. * **Chatty Services:** Excessive inter-service communication that introduces latency and complexity. ## 2. Project Structure and Organization This section describes the recommended layout for F# projects. ### 2.1 File Organization **Rationale:** A well-organized file structure improves code discoverability and maintainability. **Do This:** * Organize files by logical modules or components. * Use folders to group related files. * Name files according to the module or type they contain (e.g., "Order.fs", "OrderService.fs"). * Place interface definitions in separate files ("IOrderRepository.fs"). * Within a file, order declarations as: types, then functions depending on those types. Circular dependencies are an anti-pattern to be avoided. **Don't Do This:** * Place unrelated code in the same file. * Use excessively long or cryptic filenames. * Create deeply nested folder structures without clear purpose. **Example:** """ MyProject/ ├── Domain/ │ ├── Order.fs │ ├── Customer.fs │ └── Types.fs ├── Infrastructure/ │ ├── OrderRepository.fs │ ├── DatabaseContext.fs │ └── MessagingService.fs ├── Application/ │ ├── OrderService.fs │ └── CustomerService.fs ├── API/ │ ├── Controllers/ │ │ ├── OrderController.fs │ │ └── CustomerController.fs │ ├── Models/ │ │ ├── OrderModel.fs │ │ └── CustomerModel.fs │ └── Program.fs (or Startup.fs) └── MyProject.fsproj """ ### 2.2 Module Structure **Rationale:** Modules provide namespaces and encapsulation for F# code. **Do This:** * Use modules to group related functions and types. * Name modules according to their purpose. * Use nested modules to create hierarchies of functionality. * Consider making the module a type (record type, single case discriminated union) so it can be passed as a value. **Don't Do This:** * Create overly large modules that are difficult to navigate. * Use vague or confusing module names. * Expose internal implementation details through module exports. **Example:** """fsharp module MyModule = type MyType = { Value : int } let myFunction (x: MyType) = x.Value * 2 module Internal = let hiddenFunction x = x + 1 // Not exposed outside MyModule """ ### 2.3 Solution Structure **Rationale:** A well-structured solution facilitates code reuse and project management. **Do This:** * Divide a large application into multiple projects based on logical boundaries. * Create separate projects for core domain logic, infrastructure concerns, and UI. * Use a dedicated project for unit tests. * Consider using a "Shared" or "Common" project for utility functions and shared types. **Don't Do This:** * Create a single massive project for the entire application. * Mix unrelated code in the same project. * Create circular project dependencies. **Example:** """ MySolution/ ├── MyProject.Core/ (Domain Logic) ├── MyProject.Infrastructure/ (Data Access, Messaging) ├── MyProject.API/ (Web API) ├── MyProject.Tests/ (Unit Tests) └── MySolution.sln """ ## 3. Dependency Management and Inversion of Control This section covers dependency management techniques in F#. ### 3.1 Dependency Injection **Rationale:** Dependency Injection (DI) promotes loose coupling and testability by providing dependencies to components rather than having them create their own. **Do This:** * Use constructor injection to provide dependencies to types. * Define interfaces for dependencies to abstract away concrete implementations. * Use a DI container (e.g., .NET's built-in DI container, Autofac) to manage dependency resolution (required for larger projects, adds some initial cognitive overhead). * Consider Reader monad as an alternative to traditional DI. **Don't Do This:** * Use the 'new' keyword directly within components to create dependencies. * Create static dependencies. * Pass too many dependencies to a single type (consider refactoring). **Example:** """fsharp open Microsoft.Extensions.DependencyInjection // Interface type IEmailService = abstract member SendEmail : string -> string -> string -> unit // Implementation type EmailService() = interface IEmailService with member this.SendEmail (toAddress: string, subject: string, body: string) = printfn "Sending email to %s with subject %s: %s" toAddress subject body // Component with dependency type MyComponent(emailService: IEmailService) = member this.DoSomething() = emailService.SendEmail "user@example.com" "Important Notification" "Hello, world!" // DI Container setup let serviceCollection = ServiceCollection() serviceCollection.AddSingleton<IEmailService, EmailService>() serviceCollection.AddSingleton<MyComponent>() let serviceProvider = serviceCollection.BuildServiceProvider() // Resolve dependencies let component = serviceProvider.GetService<MyComponent>() // Usage component.DoSomething() """ ### 3.2 Reader Monad **Rationale:** The Reader Monad provides access to an environment or configuration without explicitly passing it to every function. This can be a simpler approach for smaller applications or for managing application configuration. **Do This:** * Define a type representing your dependency/configuration e.g. "type AppConfig = { DbConnectionString: string }" * Create a computation expression builder to manage the Reader context implicitly. * Use the computation expression to access your configuration within the relevant scope. **Don't Do This:** * Avoid using Reader Monad when explicit dependency injection via constructors provides clearer explicitness. * Overuse of Reader Monad to hide dependencies, making code harder to understand. **Example:** """fsharp type AppConfig = { DbConnectionString: string } // Computation Expression Builder for Reader Monad type Reader<'Config, 'T> = 'Config -> 'T type ReaderBuilder() = member this.Bind(reader: Reader<'Config, 'a>, func: 'a -> Reader<'Config, 'b>) : Reader<'Config, 'b> = fun config -> func (reader config) config member this.Return(value: 'T) : Reader<'Config, 'T> = fun _ -> value member this.ReturnFrom(reader: Reader<'Config, 'T>) : Reader<'Config, 'T> = reader member this.Zero() : Reader<'Config, unit> = fun _ -> () member this.Delay(func: unit -> Reader<'Config, 'T>) : Reader<'Config, 'T> = fun config -> (func ()) config member this.Run(reader: Reader<'Config, 'T>, config: 'Config) : 'T = reader config let reader = ReaderBuilder() // Code using the Reader Monad let getConfigValue key (config: AppConfig) = match key with | "ConnectionString" -> config.DbConnectionString | _ -> failwith "Unknown Configuration Key" let readDbData : Reader<AppConfig, string> = reader { let connectionString = getConfigValue "ConnectionString" return $"Reading data from {connectionString}" } // Application Code let appConfig = { DbConnectionString = "Server=localhost;Database=MyDb" } let data = reader.Run(readDbData, appConfig) printfn "%s" data """ ## 4. Error Handling This section focuses on robust error handling practices in F#. ### 4.1 Result Type **Rationale:** The "Result" type provides a clear and explicit way to represent operations that can succeed or fail. **Do This:** * Use the "Result<'T, 'Error>" type (or a custom discriminated union for more specific errors) to represent fallible operations. * Handle both success and failure cases explicitly using pattern matching. * Use "Result.bind" to chain operations that return "Result" values. **Don't Do This:** * Throw exceptions for expected errors. Exceptions should be reserved for truly exceptional circumstances (e.g., out-of-memory errors). * Ignore the "Error" case in a "Result" value. **Example:** """fsharp type ValidationError = | EmptyString | InvalidFormat let validateString (input: string) : Result<string, ValidationError> = if String.IsNullOrEmpty input then Error EmptyString elif input.Length < 5 then Error InvalidFormat // Example validation else Ok input let processString (validatedString: string) : Result<string, string> = try // Simulate processing Ok($"Processed: {validatedString.ToUpper()}") with _ -> Error "Processing Error" let validated = validateString "validString" let processed = Result.bind processString validated match processed with | Ok result -> printfn "Result: %s" result | Error error -> printfn "Error: %A" error """ ### 4.2 Exception Handling **Rationale:** Exceptions should be used sparingly for truly exceptional circumstances and handled carefully. **Do This:** * Use "try...with" blocks to catch and handle exceptions. * Log exceptions with meaningful context. * Avoid catching generic "Exception" unless absolutely necessary. * Consider using custom exception types for specific error conditions. **Don't Do This:** * Use exceptions for flow control. * Swallow exceptions without logging or handling them appropriately. * Rethrow exceptions without preserving the original stack trace (use "reraise()" in "use" blocks for proper resource disposal). **Example:** """fsharp open System try // Code that might throw an exception let result = 10 / 0 printfn "Result: %d" result with | :? DivideByZeroException -> printfn "Error: Division by zero" | ex -> printfn "An unexpected error occurred: %s" ex.Message """ ### 4.3 Asynchronous Error Handling **Rationale:** Asynchronous operations require special consideration for error handling using "AsyncResult". **Do This:** * Use "AsyncResult" to propagate success and failure through asynchronous workflows. * Handle exceptions within "async" blocks using "try...with". * Use "Async.Catch" to turn an "async" computation into a "Result" that signals errors (especially interacting with .NET libraries.) **Don't Skip:** * Forgetting to consider asynchronous error handling. This leads to unhandled exceptions crashing the application. **Example:** """fsharp open System let fetchUserDataAsync (userId: int): Async<Result<string, string>> = async { try // Simulate fetching user data if userId = 1 then return Ok "User Data" else failwith "User not found" with | ex -> return Error (ex.Message) } let processUserDataAsync (result: Result<string, string>): Async<unit> = async { match result with | Ok data -> printfn "User data: %s" data | Error err -> printfn "Error: %s" err } let workflow userId = fetchUserDataAsync userId |> Async.Bind processUserDataAsync Async.RunSynchronously (workflow 1) Async.RunSynchronously (workflow 2) """ ## 5. Conclusion These core architecture standards for F# offer a robust foundation for building maintainable, scalable, and performant applications. Adhering to these guidelines will promote consistency within development teams and facilitate the creation of high-quality F# code. Regularly review and adapt these standards to remain aligned with evolving best practices and project-specific needs.
# Tooling and Ecosystem Standards for F# This document outlines the recommended tooling and ecosystem standards for F# development. Adhering to these standards will promote consistency, maintainability, and performance in F# projects. It is designed to guide developers and inform AI coding assistants such as GitHub Copilot and Cursor. ## 1. Development Environment and Tools ### 1.1 IDEs and Editors **Do This:** * **Use Visual Studio:** Visual Studio provides excellent F# support, including IntelliSense, debugging, and project management. It has excellent refactoring capabilities and integration with the .NET ecosystem. * **Use VS Code with Ionide:** VS Code with the Ionide extension delivers a lightweight, cross-platform F# development experience. Ionide provides syntax highlighting, IntelliSense, and build integration. **Don't Do This:** * **Rely solely on basic text editors:** Basic text editors lack the features necessary for efficient F# development, such as type checking and debugging. **Why:** * IDEs and advanced editors significantly improve developer productivity by providing real-time feedback, refactoring tools, and debugging capabilities. **Example (Visual Studio):** """fsharp // Navigating code using IntelliSense let add x y = x + y let result = add 5 3 // Press Ctrl+Space to see available functions and variables """ **Example (VS Code with Ionide):** """fsharp // Using Ionide to quickly find type information let greet name = printfn "Hello, %s!" name // Hover over 'name' to see its type greet "World" """ ### 1.2 Build Tools **Do This:** * **Use "dotnet build" or "msbuild":** These are the standard .NET build tools and are well-integrated with F# projects. "dotnet build" generally provides a simpler experience. * **Use Paket for dependency management:** Paket is a robust dependency manager specifically designed for F#. It supports advanced features like version constraints and dependency groups. * **Consider using FAKE for build automation:** FAKE provides a DSL for writing build scripts in F#. It offers features like compilation, testing, and deployment. * **Integrate with CI/CD:** Ensure your build process is integrated with a continuous integration and continuous deployment pipeline (e.g., Azure DevOps, GitHub Actions). **Don't Do This:** * **Manually manage dependencies:** Manual dependency management is error-prone and difficult to maintain. * **Rely on ad-hoc build scripts:** Ad-hoc build scripts are often fragile and difficult to understand. **Why:** * Using standardized build tools and dependency management helps automate the build process, ensures repeatability, and reduces the risk of errors. * CI/CD integration enables automatic builds, tests, and deployments, improving code quality and release frequency. **Example ("dotnet build"):** """bash dotnet build """ **Example (Paket):** """paket source nuget https://api.nuget.org/v3/index.json nuget FSharp.Core nuget FSharp.Data nuget NUnit """ **Example (FAKE):** """fsharp #r "paket:r://FAKE.Core.FileSystem" #r "paket:r://FAKE.Core.Process" #r "paket:r://FAKE.DotNet.MSBuild" #r "paket:r://FAKE.Testing.NUnit" open Fake.Core open Fake.Core.Process open Fake.DotNet open Fake.Testing.NUnit open Fake.FileSystem // Target names let Clean = "Clean" let Build = "Build" let Test = "Test" let Deploy = "Deploy" // Properties let solutionFile = "./MySolution.sln" let buildDir = "./build" let testDir = "./test" // Helper function to execute a code block and log exceptions to console let safeExecute onError f = try f () with e -> printfn "Failed: %s" (e.Message + "\n" + e.StackTrace) onError () // Helper function to build a dotnet project using msbuild let buildProject (projectFile: string) = safeExecute (fun () -> failwith "Build failed") (fun () -> MSBuild solutionFile (fun msbuild -> { msbuild with Verbosity = MSBuildVerbosity.Minimal Properties = [ "Configuration", "Release" ] } ) |> ignore ) // Helper function to run nunit tests let runTests testAssembly = safeExecute (fun () -> failwith "Test failed") (fun () -> NUnit [testAssembly] |> ignore) Target Clean (fun _ -> CleanDir buildDir ) Target Build (fun _ -> buildProject solutionFile ) Target Test (fun _ -> runTests (Path.Combine buildDir (Path.GetFileNameWithoutExtension solutionFile) + ".Tests.dll") ) Target Deploy (fun _ -> // Deployment logic here trace "Deploying..." ) // Define build order "Clean" ==> "Build" ==> "Test" ==> "Deploy" // Start build RunTargetOrDefault "Build" """ ### 1.3 Debugging **Do This:** * **Use the Visual Studio debugger:** Visual Studio provides a comprehensive debugger for F#, including breakpoints, stepping, and variable inspection. * **Use the VS Code debugger:** The VS Code debugger, alongside the Ionide extension, supports debugging F# code. * **Use logging:** Employ logging libraries like "Serilog" or built-in "System.Diagnostics.Debug" for tracing execution and diagnosing issues. * **Consider using FsReveal for live code exploration:** FsReveal enables you to visualize data structures and code execution in real-time. **Don't Do This:** * **Rely solely on "printfn" statements for debugging:** While "printfn" can be useful for quick debugging, it's not a substitute for a proper debugger. **Why:** * Debuggers allow you to step through code, inspect variables, and identify the root cause of errors. * Logging provides a record of the program's execution, which can be invaluable for diagnosing issues in production. **Example (Visual Studio Debugger):** """fsharp // Setting a breakpoint in Visual Studio let divide x y = if y = 0 then failwith "Division by zero" else x / y let result = divide 10 2 // Set a breakpoint here """ **Example (Serilog):** """fsharp open Serilog let logger = LoggerConfiguration() .WriteTo.Console() .CreateLogger() let add x y = logger.Information("Adding {x} and {y}", x, y) let result = x + y logger.Debug("Result: {result}", result) result let main() = add 5 3 |> ignore logger.CloseAndFlush() main() """ ### 1.4 Testing **Do This:** * **Use a testing framework:** NUnit, xUnit, and FsCheck are popular testing frameworks for F#. * **Write unit tests for all critical functions:** Unit tests verify that individual functions behave as expected. * **Use property-based testing with FsCheck:** FsCheck automatically generates test cases based on properties of your functions. * **Perform integration tests:** Integration tests verify that different parts of the system work together correctly. * **Employ mocking frameworks like Moq or NSubstitute when necessary:** Carefully consider whether mocking is truly necessary, as it can sometimes make tests harder to maintain. Often using abstractions directly is preferred. **Don't Do This:** * **Skip testing altogether:** Untested code is more likely to contain bugs. * **Write only positive tests:** Test both positive and negative scenarios to ensure robustness. **Why:** * Testing helps catch bugs early, improves code quality, and makes it easier to refactor code. * Property-based testing can uncover edge cases that you might not have considered manually. **Example (NUnit):** """fsharp open NUnit.Framework [<TestFixture>] type MathTests() = [<Test>] member this.""Add should return the sum of two numbers""() = Assert.AreEqual(5, 2 + 3) [<Test>] member this.""Divide should throw an exception when dividing by zero""() = Assert.Throws<System.Exception>(fun () -> 10 / 0 |> ignore) |> ignore """ **Example (FsCheck):** """fsharp open FsCheck let add x y = x + y [<Property>] let ""Add is commutative"" (x: int) (y: int) = add x y = add y x Check.QuickThrowOnFailure ""Add is commutative"" """ ## 2. Libraries and Frameworks ### 2.1 Core Libraries **Do This:** * **Use "FSharp.Core":** This library is the foundation of F# and provides essential types and functions. * **Use "System":** The standard .NET library provides a wide range of functionality, including I/O, networking, and collections. **Don't Do This:** * **Avoid rewriting functionality already provided by core libraries:** Leverage existing libraries to reduce code duplication and improve maintainability. **Why:** * Core libraries are well-tested and optimized, providing reliable and efficient functionality. **Example:** """fsharp // Using FSharp.Core for option types let maybeValue: int option = Some 10 // Using System for file I/O let filePath = "data.txt" System.IO.File.WriteAllText(filePath, "Hello, world!") """ ### 2.2 Data Access **Do This:** * **Use "FSharp.Data" for JSON and XML parsing:** "FSharp.Data" provides type providers for working with JSON and XML data. * **Use an ORM like Dapper or Entity Framework Core :** for relational database access. Consider "SQLProvider" to create a type-safe abstraction and compile-time checkable access to your database. **Don't Do This:** * **Use string concatenation to build SQL queries:** String concatenation is vulnerable to SQL injection attacks. * **Ignore proper error handling when accessing data:** Handle potential exceptions and errors gracefully. **Why:** * Type providers offer compile-time type checking, making data access more robust and less error-prone. * ORMs simplify database interactions and prevent SQL injection vulnerabilities. **Example ("FSharp.Data" with JSON):** """fsharp open FSharp.Data type Weather = JsonProvider<"http://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY"> let weather = Weather.Load("http://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY") let temperature = weather.Main.Temp printfn "Temperature in London: %f" temperature """ **Example (Dapper):** """fsharp open Dapper open System.Data.SqlClient let connectionString = "Data Source=.;Initial Catalog=MyDatabase;Integrated Security=True" let getAllProducts() = use connection = new SqlConnection(connectionString) connection.Query<{| Id : int; Name : string; Price : decimal |}>("SELECT Id, Name, Price FROM Products") let products = getAllProducts() """ **Example (SQLProvider):** """fsharp //Requires SqlProvider package #r "nuget:SQLProvider" #r "System.Data.SqlClient" open FSharp.Data.Sql type Sql = SqlDataProvider< Common.DatabaseType.MsSql, Common.Schema.ProvidedSchemeOptions.Default, ConnectionString = "Data Source=.;Initial Catalog=MyDatabase;Integrated Security=True;"> let ctx = Sql.GetDataContext() let products = ctx.""Sql.Product"".AsyncGetAll() |> Async.RunSynchronously products |> Seq.iter(fun p -> printfn "%s" p.ProductName) """ ### 2.3 Web Development **Do This:** * **Use ASP.NET Core with F#:** ASP.NET Core provides a modern, cross-platform web framework for building web applications and APIs. * **Consider using Giraffe or Saturn:** These are F#-specific web frameworks that provide a functional approach to web development. * **Use Swagger/OpenAPI for API documentation:** Swagger/OpenAPI makes your API navigable and automatically generates client SDKs. **Don't Do This:** * **Rely on older web frameworks without strong functional programming support:** Steer clear of legacy frameworks that do not integrate well with F#'s functional paradigm. **Why:** * ASP.NET Core and F#-specific frameworks offer a good balance of features, performance, and functional programming support. * Swagger/OpenAPI simplifies API consumption and integration. **Example (Giraffe):** """fsharp open Giraffe open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Hosting open Microsoft.Extensions.DependencyInjection let webApp = choose [ GET >=> path "/" >=> text "Hello from Giraffe!" ] let configureServices (services: IServiceCollection) = services.AddGiraffe() |> ignore let configureApp (app: IApplicationBuilder) (env: IWebHostEnvironment) = app.UseGiraffe webApp |> ignore let configureHostBuilder (builder: IHostBuilder) = builder.ConfigureWebHostDefaults(fun webBuilder -> webBuilder.UseStartup(fun _ -> {| ConfigureServices = configureServices; Configure = configureApp |}) ) |> ignore [<EntryPoint>] let main argv = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(argv) .ConfigureWebHostDefaults(fun webBuilder -> webBuilder.UseStartup(fun _ -> {| ConfigureServices = configureServices; Configure = configureApp |})) |> configureHostBuilder |> Microsoft.Extensions.Hosting.Host.Build |> Microsoft.Extensions.Hosting.Host.Run 0 """ ### 2.4 Asynchronous Programming **Do This:** * **Use "async" and "Async.Await" for asynchronous operations:** These keywords provide a clear and concise way to write asynchronous code. * **Utilize "Task<'T>" when interoperating with C# code:** Tasks are the standard asynchronous programming model in .NET, ensuring seamless integration between F# and C# projects. * **Handle exceptions in asynchronous workflows:** Use "try...finally" blocks to ensure proper cleanup, even in the presence of exceptions. **Don't Do This:** * **Block on asynchronous operations:** Blocking can lead to deadlocks and performance issues. * **Ignore exception handling in asynchronous workflows:** Unhandled exceptions can crash your application. **Why:** * Asynchronous programming allows you to write non-blocking code, improving application responsiveness and scalability. **Example:** """fsharp open System.Net.Http let downloadUrlAsync (url: string) = async { use client = new HttpClient() let! response = client.GetAsync url |> Async.AwaitTask response.EnsureSuccessStatusCode() |> ignore let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask return content } let main() = async { try let! result = downloadUrlAsync "https://www.example.com" printfn "Downloaded: %s" (result.Substring(0, 100)) with ex -> printfn "Error: %s" ex.Message } |> Async.RunSynchronously main() """ ### 2.5 Concurrency and Parallelism **Do This:** * **Use "Async.Parallel" or "Parallel.ForEach" when performing CPU-bound operations.:** These functions allow you to execute tasks in parallel. Pay close attention that no mutable or shared state is being updated which can lead to unpredictable and erroneous behavior. * **Consider using "MailboxProcessor" for managing concurrent state:** "MailboxProcessor" provides a safe and reliable way to handle shared mutable state in concurrent applications. * **Use "ConcurrentDictionary" for thread-safe collections:** "ConcurrentDictionary" provides thread-safe access to dictionary data. **Don't Do This:** * **Access shared mutable state without proper synchronization:** Unsynchronized access can lead to race conditions and data corruption. * **Overuse parallelism:** Parallelism can add overhead. Ensure benefits outweigh the cost by using a profiler. **Why:** * Concurrency and parallelism allow you to utilize multiple cores, improving performance. * Proper synchronization and thread-safe data structures are essential for writing correct concurrent code. **Example ("Async.Parallel"):** """fsharp let processDataAsync (data: int[]) = data |> Array.map (fun x -> async { printfn "Processing %d on thread %d" x System.Threading.Thread.CurrentThread.ManagedThreadId do! Async.Sleep 100 // Simulate some work return x * x }) |> Async.Parallel |> Async.RunSynchronously """ **Example ("MailboxProcessor"):** """fsharp open System open System.Threading open Microsoft.FSharp.Control type Message = | Increment | GetValue of AsyncReplyChannel<int> let agent () = MailboxProcessor.Start (fun inbox -> let rec loop state = async { let! msg = inbox.Receive() match msg with | Increment -> printfn "Incrementing" return! loop (state + 1) | GetValue replyChannel -> printfn "Getting value: %d" state replyChannel.Reply state return! loop state } loop 0) [<EntryPoint>] let main argv = let myAgent = agent() myAgent.Post Increment myAgent.Post Increment let getValue() = myAgent.PostAndReply(fun (replyChannel: AsyncReplyChannel<int>) -> GetValue replyChannel) Thread.Sleep 100 // let agent process increments first. printfn "The value is %d" (getValue()) 0 """ ## 3. Code Analysis and Formatting ### 3.1 Static Analysis **Do This:** * **Use FSharpLint :** Run the "fsharplint" tool configured with project-specific linting rules. * **Enable all relevant warnings in your project file:** Pay attention to warnings and fix them promptly. **Don't Do This:** * **Ignore compiler warnings:** Warnings can indicate potential problems in your code. **Why:** * Static analysis helps identify potential bugs, code smells, and style violations. **Example (Enabling warnings):** """xml <PropertyGroup> <WarningLevel>5</WarningLevel> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> """ ### 3.2 Code Formatting **Do This:** * **Use Fantomas for automatic code formatting:** Fantomas automatically formats your F# code according to predefined rules. This is critical for enforcing standards and consistency across your code base. **Don't Do This:** * **Rely on manual code formatting:** Manual formatting is time-consuming and error-prone. * **Disable Fantomas without a good reason:** Disabling Fantomas can lead to inconsistent code formatting. **Why:** * Automatic code formatting ensures that all code adheres to a consistent style, improving readability and maintainability. **Example (Fantomas configuration - .editorconfig):** """ini root = true [*] indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.fs] fsharp_indent_style = normal """ ## 4. Documentation ### 4.1 API Documentation **Do This:** * **Use triple-slash comments to document public APIs:** Triple-slash comments are used to generate API documentation. Provide examples in documentation - a common way for developer to learn a new API is just to use the samples. * **Use tools like "FSharp.Formatting" or "DocFX" to generate documentation websites:** These tools can automatically generate documentation from your code and comments. **Don't Do This:** * **Skip documenting public APIs:** Undocumented APIs are difficult to understand and use. * **Write ambiguous or incomplete documentation:** Documentation should be clear, concise, and accurate. **Why:** * API documentation makes it easier for developers to understand and use your code. **Example (Triple-slash comments):** """fsharp /// <summary> /// Adds two numbers together. /// </summary> /// <param name="x">The first number.</param> /// <param name="y">The second number.</param> /// <returns>The sum of the two numbers.</returns> let add x y = x + y """ ### 4.2 Internal Documentation **Do This:** * **Document complex or non-obvious code:** Add comments to explain the purpose and behavior of complex code. * **Use meaningful names for variables and functions:** Self-documenting code is easier to understand. * **Keep comments up-to-date:** Outdated comments can be misleading. **Don't Do This:** * **Write comments that state the obvious:** Comments should explain *why* the code is doing something, not *what* it is doing. * **Over-comment code:** Excessive commenting can make code harder to read. **Why:** * Internal documentation helps other developers (and your future self) understand the code. **Example:** """fsharp // Calculate the average of a list of numbers let average numbers = //Guard against empty lists, don't want to divide by zero if numbers.Length = 0 then None else let sum = numbers |> Array.sum Some (float sum / float numbers.Length) """ ## 5. Versioning & Release ### 5.1 Semantic Versioning **Do This:** * **Employ semantic versioning (SemVer):** Use the MAJOR.MINOR.PATCH format to indicate the type of changes in each release. * **Use clear release notes:** Explain new features, bug fixes, and breaking changes. **Don't Do This:** * **Make breaking changes without bumping the major version:** Breaking changes can cause compatibility issues for consumers of your code. **Why:** * Versioning helps consumers understand the impact of upgrading to a new version of your code. **Example:** * "1.0.0": Initial release * "1.1.0": Added a new feature (non-breaking) * "1.0.1": Fixed a bug * "2.0.0": Introduced a breaking change ### 5.2 Package Management **Do This:** * **Publish packages to NuGet or a private feed:** NuGet is the standard package manager for .NET. * **Include a README file with instructions on how to use your package:** Provide clear and concise instructions, examples and getting starting guide. * **Sign your packages to ensure integrity:** Package signing guarantees that the package hasn't been tampered with. **Don't Do This:** * **Publish broken or incomplete packages:** Test your packages thoroughly before publishing. * **Publish packages without proper metadata:** Include a description, keywords, and license information. **Why:** * Package management makes it easy for developers to install and use your code. This document provides a comprehensive set of tooling and ecosystem standards for F# development. Adhering to these standards will improve your F# code quality, maintainability, and performance. Regularly review and update these standards to stay current with the latest best practices.
# Testing Methodologies Standards for F# This document outlines the testing methodologies standards for F# development. It aims to provide guidance on unit, integration, and end-to-end testing, tailored for F# and incorporating modern best practices. These standards emphasize testability, maintainability, and clarity. ## 1. General Testing Principles ### 1.1. Test Pyramid Adaptation for Functional Programming Testing should be structured around a testing pyramid, but with adaptations to suit functional programming paradigms. Focus on unit tests at the base, integration tests in the middle, and end-to-end tests at the top. Minimize UI-driven end-to-end tests in favor of integration tests that focus on interacting layers of your application (functional core). * **Do This:** Prioritize unit tests to cover core business logic. Utilize property-based testing to explore edge cases. Increase use of integration tests to verify interactions between modules or components. Limit end-to-end tests to essential user flows. * **Don't Do This:** Rely heavily on end-to-end tests for all functionality. Neglect unit tests for business logic. Skip integration tests between function-oriented modules. **Why:** Unit tests are easier to write, faster to execute, and provide rapid feedback. Integration tests provide reasonable coverage with less overhead than E2E tests. Functional programming promotes testability via pure functions and immutable data, making unit testing more effective. ### 1.2. First-Class Tests Tests should be treated as first-class citizens in your codebase. Structure tests with the same care you take in structuring your production code. * **Do This:** Name test functions clearly to describe their intent. Organize tests into logical modules or files. Separate test data from test logic. * **Don't Do This:** Write tests that are poorly named, disorganized, or tightly coupled to implementation details. **Why:** Clean, well-organized tests are easier to understand, maintain, and extend, reducing technical debt. ### 1.3. Property-Based Testing Leverage property-based testing using libraries like FsCheck to automatically generate test cases and uncover edge cases. * **Do This:** Use FsCheck or similar libraries to define properties that should hold true for a range of inputs. * **Don't Do This:** Rely solely on example-based testing, which can miss corner cases. **Why:** Property-based testing provides broader coverage and helps discover unexpected behavior, improving the robustness of your code. """fsharp open FsCheck open FsCheck.Xunit [<Property>] let ""String concatenation is associative"" (s1: string, s2: string, s3: string) = (s1 + s2) + s3 = s1 + (s2 + s3) """ ### 1.4 Test-Driven Development (TDD) and Behavior-Driven Development (BDD) Adopt Test-Driven Development (TDD) or Behavior-Driven Development (BDD) to improve code design and ensure testability. * **Do This:** Write failing tests *before* implementing the corresponding functionality (Red-Green-Refactor). Use BDD frameworks (e.g., Expecto) to write tests that describe the expected behavior of your system. * **Don't Do This:** Write tests after implementation (or not at all). Ignore error cases. **Why:** TDD drives better design and encourages testability from the outset. BDD provides a clear specification of expected behavior. ## 2. Unit Testing Standards ### 2.1. Isolation & Purity Strive for pure functions (functions without side effects) wherever possible. Pure functions are trivially unit testable. * **Do This:** Minimize mutable state, avoid I/O in core business logic, and inject dependencies to control side effects. * **Don't Do This:** Write functions that have dependencies on global state or perform I/O directly. Build dependencies directly into the core of your functions. **Why:** Pure functions are deterministic and isolated, making them easy to test by simply verifying their return values. """fsharp // Pure function: easy to test let add x y = x + y // impure function (depends on external state): harder to test directly let addWithLog x y = let result = x + y printfn $"Result: {result}" // Side effect: I/O result """ ### 2.2. Dependency Injection Use dependency injection to pass dependencies into functions or modules, enabling easy mocking and stubbing during unit testing. * **Do This:** Pass external dependencies as function arguments or constructor parameters. Use interfaces or abstract types to define contracts for dependencies. Libraries like "NSubstitute" can be useful for creating mocks. * **Don't Do This:** Hardcode dependencies within functions or modules. Use concrete types directly without interfaces. **Why:** Dependency injection promotes loose coupling, improving testability and making your code more modular and reusable. """fsharp // Interface for a data access layer type IDataService = abstract member GetData: unit -> string // Implementation of the data access layer type DataService(connectionString: string) = interface IDataService with member this.GetData() = // Simulate fetching data from a database using the connection string $"Data from {connectionString}" // Function that depends on IDataService let processData (dataService: IDataService) = let data = dataService.GetData() $"Processed: {data}" // Unit test using mocking with NSubstitute open NSubstitute open Xunit [<Fact>] let ""processData should process data from the data service"" () = // Arrange let mockDataService = Substitute.For<IDataService>() mockDataService.GetData().Returns("TestData") // Act let result = processData mockDataService // Assert Assert.Equal("Processed: TestData", result) """ ### 2.3. Mocking and Stubbing Use mocking frameworks to isolate units of code by replacing their dependencies with controlled test doubles. * **Do This:** Use mocking libraries like NSubstitute or Moq for .NET or create simple mocks manually using function literals. * **Don't Do This:** Avoid mocking implementation details. Focus on mocking interfaces and abstract types. **Why:** Mocking allows you to test units of code in isolation, verifying their interactions with dependencies. """fsharp // Example using a simple manual mock let testProcessData dataServiceGetDataResult = let dataServiceGetData () = dataServiceGetDataResult let dataService = { new IDataService with member this.GetData () = dataServiceGetData() } processData dataService """ ### 2.4. Testing Higher-Order Functions When testing higher-order functions, verify the behavior of both the higher-order function itself and the functions passed as arguments. * **Do This:** Test that the correct function is called with the expected arguments. Use mocking to verify function invocations. * **Don't Do This:** Only test the return value of the higher-order function without considering the behavior of the functions it uses. **Why:** Higher-order functions are a powerful feature of functional programming. Thoroughly testing them ensures their correct behavior and the proper execution of their function arguments. """fsharp // Higher-order function example let mapAndProcess (list: 'a list) (mapper: 'a -> 'b) (processor: 'b -> unit) = list |> List.map mapper |> List.iter processor // Test example (using NSubstitute) [<Fact>] let ""mapAndProcess calls mapper and processor correctly"" () = let list = [ 1; 2; 3 ] let mockMapper = Substitute.For<Func<int, string>>() let mockProcessor = Substitute.For<Action<string>>() mapAndProcess list mockMapper.Invoke mockProcessor.Invoke mockMapper.Received(3).Invoke(Arg.Any<int>()) mockProcessor.Received(3).Invoke(Arg.Any<string>()) """ ## 3. Integration Testing Standards ### 3.1. Component Boundaries Integration tests should focus on interactions between different components or modules within your application. * **Do This:** Identify clear component boundaries (e.g., modules interacting using message passing or function composition). Write tests to verify that these components communicate correctly. * **Don't Do This:** Write integration tests that are too broad, covering the entire application. **Why:** Focusing on component boundaries allows you to test specific interactions and isolate problems effectively. ### 3.2. Real Dependencies (When Appropriate) In some cases, integration tests may use real dependencies like databases or message queues. * **Do This:** Use lightweight test databases or message queues. Clean up test data after each test run. Containerization technologies (Docker) can greatly assist in this. * **Don't Do This:** Use production databases or message queues for integration tests. Leave test data polluting real environments. **Why:** Using real dependencies provides more realistic test scenarios, but cleaning up after tests prevents side effects. ### 3.3 Testing Module Composition F# frequently utilizes module composition to build applications. Integration tests should confirm appropriate interactions between modules. * **Do This:** Build tests that specifically check that calling functions in one module correctly triggers behaviors in other modules that are intended to respond. * **Don't Do This:** Focus on module composition at the exclusion of per-module unit tests. **Why:** Effectively composed modules are the cornerstone of a well-architected functional F# application. """fsharp // Module definitions module ModuleA = let processInput input = // Some processing logic let result = $"ModuleA processed: {input}" ModuleB.handleResult result module ModuleB = let mutable receivedResults = [] let handleResult result = // Simulate handling the processed result receivedResults <- result :: receivedResults """ Example Integration Test (XUnit) """fsharp open Xunit [<Fact>] let ""ModuleA should process input and trigger ModuleB's handler"" () = // Arrange ModuleB.receivedResults <- [] // Reset state // Act ModuleA.processInput "TestInput" // Assert Assert.Contains("ModuleA processed: TestInput", ModuleB.receivedResults) """ ### 3.4 Testing Asynchronous Code Special care must be taken to correctly test asynchronous code. * **Do This:** Always use "async { ... }" blocks when testing asynchronous operations. Use "Async.RunSynchronously" or equivalent methods to execute asynchronous code in a synchronous testing context. Use "Task.Delay" or "Async.Sleep" for testing timeouts. * **Don't Do This:** Block on asynchronous operations by calling ".Result" or ".Wait()" directly because this can lead to deadlocks. Forget to await asynchronous operations. **Why:** Correctly testing asynchronous code is essential for ensuring reliability, preventing deadlocks, and handling timeouts. """fsharp open Xunit open System.Threading.Tasks [<Fact>] let ""Async operation should complete successfully"" () = let asyncOperation () = async { do! Async.Sleep 100 // Simulate an asynchronous operation return "Success" } let result = Async.RunSynchronously (asyncOperation()) Assert.Equal("Success", result) """ ### 3.5 Validating Message Passing When dealing with event-driven or message-passing architectures, use integration tests to affirm that messages are correctly passed between components. * **Do This:** Create subscribers that listen to channels/queues. Check messages are serialized/deserialized properly. Mock external services required for message processing. * **Don't Do This:** Skip message validation testing. Fail to handle different message versions. **Why:** Message passing correctness is critical for these types of systems. Testing this directly via integration tests makes failures highly visible. ## 4. End-to-End (E2E) Testing Standards ### 4.1. Limited Scope E2E tests should cover only critical user flows or business scenarios, ensuring that the system works as a whole. * **Do This:** Focus on testing the most important user journeys. Keep the number of E2E tests relatively small. * **Don't Do This:** Write E2E tests for every feature or functionality. This will make the test suite slow and brittle. **Why:** E2E tests are expensive to write and maintain. Focusing on key flows provides the most value. ### 4.2. Realistic Environments Run E2E tests in environments that closely resemble production, including dependencies like databases and external services. * **Do This:** Use staging environments for E2E testing. Configure the test environment to match production configurations. * **Don't Do This:** Run E2E tests in local development environments or against mocked services. **Why:** Realistic environments help catch issues that may not be apparent in development or integration environments. ### 4.3. Test Automation Automate E2E tests to run as part of your continuous integration/continuous delivery (CI/CD) pipeline. * **Do This:** Integrate E2E tests with your CI/CD system. Schedule tests to run regularly (e.g., daily or nightly). * **Don't Do This:** Run E2E tests manually or infrequently. **Why:** Automated E2E tests provide timely feedback on the overall health of your system. ### 4.4 UI Test Automation For applications with user interfaces, employ UI automation tools like Selenium or Playwright (via .NET bindings). * **Do This:** Use page object patterns to represent UI elements and interactions. Write tests that simulate user actions. * **Don't Do This:** Write brittle tests that directly depend on UI implementation details. **Why:** UI automation allows you to test the application from a user's perspective. """fsharp // Example of using Selenium WebDriver (.NET bindings assumed) open OpenQA.Selenium open OpenQA.Selenium.Chrome open Xunit [<Fact>] let ""User can login successfully"" () = // Arrange let options = ChromeOptions() options.AddArgument("--headless") // Run Chrome in headless mode use driver = new ChromeDriver(options) // Act driver.Navigate().GoToUrl("http://example.com/login") let usernameField = driver.FindElement(By.Id "username") usernameField.SendKeys("testuser") let passwordField = driver.FindElement(By.Id "password") passwordField.SendKeys("password") let loginButton = driver.FindElement(By.Id "loginButton") loginButton.Click() // Assert let welcomeMessage = driver.FindElement(By.Id "welcomeMessage") Assert.Equal("Welcome, testuser!", welcomeMessage.Text) """ ## 5. Test Data Management ### 5.1. Test Data Generation Employ tools or strategies that generate realistic and diverse test data. * **Do This:** Utilize libraries to produce random data (names, addresses, numbers). Build factory functions to create complex test objects. Consider PBT techniques extended with data generation. * **Don't Do This:** Rely only on hardcoded sample data. Avoid covering edge and boundary cases. **Why:** Reliable and various test data increases test coverage and uncovers potential defects. ### 5.2. Data Isolation Ensure test data is isolated to prevent tests influencing each other (stateful tests). * **Do This:** Reset state, before and after tests. Use transaction scopes that roll back changes. Create temporary DB objects automatically. * **Don't Do This:** Share resources modifying them without resetting the state or using immutable data structure copies. Make tests depend on execution order. **Why:** Data isolation ensures tests are predictable and replicable. ### 5.3. Data Anonymization For tests that use sensitive data, anonymize it when possible with appropriate tools. * **Do This:** Employ libraries implementing data masking policies. Define a strategy how to deal with PII/PHI data. * **Don't Do This:** Use clear, identifying information in test systems. Skip compliance aspects for private data. **Why:** Data anonymization reduces the risk of data breaches and ensures compliance with regulations. ## 6. Performance Testing While not strictly "methodology", performance testing is an important aspect to consider alongside functional testing. ### 6.1 Profiling Use profiling tools to identify performance bottlenecks * **Do This:** Utilize tools such as JetBrains dotTrace, or PerfView to thoroughly analyze the runtime behavior of your code. Pay special attention to memory allocations. * **Don't Do This:** Assume the cause of performance issues. Try to guess where the bottlenecks are. **Why:** Profiling gives you concrete evidence of performance bottlenecks which guides focused optimization efforts. ### 6.2 Benchmarking Write benchmarks to quantitatively assess the performance of code. * **Do This:** Employ the BenchmarkDotNet library to obtain precise timing metrics and statistics. Use a repeatable process to run the benchmarks. * **Don't Do This:** Rely on imprecise timing mechanisms. Disregard statistical variability. **Why:** Benchmarks provide objective assessments to guide performance tuning. ### 6.3 Load Testing Simulate production traffic patterns to assess the capability of your application. * **Do This:** Simulate realistically scaled user populations hitting the system concurrently. Monitor resource utilization. * **Don't Do This:** Load test an unprepared environment. Omit key performance metrics. **Why:** Load tests determine whether the application is scalable & identify bottlenecks under load. ## 7. Security Testing Security testing is a critical component of overall software quality. ### 7.1 Static Analysis Security Testing (SAST) Apply SAST to identify vulnerabilities during development * **Do This:** Utilize tools like SonarQube and integrate with CI/CD processes to scan for potential weaknesses. * **Don't Do This:** Ignore findings from SAST. Neglect to review generated reports. **Why:** Provides early detection of common security issues ### 7.2 Dynamic Analysis Security Testing (DAST) Test the running application for exploitable risks. * **Do This:** Perform penetration testing to pinpoint runtime security issues. * **Don't Do This:** Deploy without any sort of thorough security assessment. **Why:** Discovers vulnerabilities only detectable in a live environment ### 7.3 Dependency Scanning Scan dependencies for known vulnerabilities * **Do This:** Employ tools such as OWASP Dependency-Check. Monitor for alerts on vulnerable components. Use package managers (like NuGet) security features * **Don't Do This:** Trust all dependencies without proper evaluation. Ignore reported vulnerabilities. **Why:** Prevents utilizing libraries with known critical exploits. By adhering to these standards, you can ensure maintainable, performant, reliable, and secure F# code, improving developer productivity and delivering high-quality software.
# Performance Optimization Standards for F# This document outlines performance optimization standards for F# code. It provides guidelines and best practices to improve application speed, responsiveness, and resource usage, specifically within the F# ecosystem. These standards are tailored to the latest version of F# and emphasize modern approaches. ## 1. Understanding Performance in F# ### 1.1. Core Principles * **Do This:** Prioritize immutability and functional programming principles. While not inherently faster, immutability drastically simplifies reasoning about code and reduces the risk of side effects, which can lead to subtle performance bugs. * **Don't Do This:** Focus solely on micro-optimizations early on. Profile your code first to identify bottlenecks. Premature optimization is the root of all evil. * **Why:** Functional programming encourages writing pure functions, which are easier to optimize by the compiler and runtime. ### 1.2. Performance Factors Specific to F# * **Immutability and Data Structures:** F#'s default immutability can lead to allocations if not handled correctly. * **Collection Types:** Choosing appropriate collection types is crucial. Lists are good for prepending, arrays for random access, and sequences for lazy evaluation. * **Discriminated Unions (DUs):** Clever use of DUs can improve performance through optimized pattern matching. * **Computation Expressions:** While providing elegant syntax, be mindful of the overhead introduced by nested computation expressions. * **Interoperability with .NET:** Leverage .NET libraries for highly optimized routines where necessary (e.g., "System.Numerics" for vector operations). * **Asynchronous Programming:** Proper use of "async" and "task" computation expressions is crucial for responsiveness in I/O and CPU-bound operations. ## 2. Design and Architecture ### 2.1. Choosing the Right Algorithms and Data Structures * **Do This:** Carefully consider the time and space complexity of algorithms when choosing them. * **Do This:** Select appropriate data structures based on the needs of the application (e.g., use "Set" for quick membership testing, "Map" for key-value lookups, "Array" for indexed access). Consider immutable collections from "FSharp.Collections.Immutable" for thread safety and efficiency in concurrent scenarios. * **Don't Do This:** Default to using "List" for everything. Analyze access patterns to determine the optimal data structure. * **Why:** The choice of algorithm and data structure has the most significant impact on performance. """fsharp // Example: Choosing between List and Set for membership testing let data = [1..100000] // Inefficient: Linear search through a list let listContains x = data |> List.contains x // Efficient: Constant-time lookup in a set let dataSet = Set.ofList data let setContains x = dataSet.Contains x // Time the execution let sw = System.Diagnostics.Stopwatch.StartNew() for i in 1..1000 do listContains 50000 |> ignore sw.Stop() printfn "List contains took: %A ms" sw.ElapsedMilliseconds sw.Restart() for i in 1..1000 do setContains 50000 |> ignore sw.Stop() printfn "Set contains took: %A ms" sw.ElapsedMilliseconds """ ### 2.2. Minimizing Garbage Collection Pressure * **Do This:** Strive to reduce allocations. Use mutable structures judiciously *where profiling shows a significant benefit*, understanding the trade-offs. * **Do This:** Employ object pooling for frequently created and destroyed objects (using "System.Collections.Concurrent.ConcurrentBag" is a good starting point). * **Don't Do This:** Create excessive temporary objects, especially in tight loops. * **Why:** Frequent garbage collection pauses can significantly impact application responsiveness. """fsharp // Example: Object pooling open System.Collections.Concurrent type ReusableObject() = member val Data = "" with get, set let objectPool = ConcurrentBag<ReusableObject>() let getObject () : ReusableObject = match objectPool.TryTake() with | true, obj -> obj | false, _ -> ReusableObject() let returnObject (obj: ReusableObject) = obj.Data <- "" // Reset object state objectPool.Add obj let processData () = let obj = getObject() obj.Data <- "Some data" // ... process data ... returnObject obj """ ### 2.3. Concurrency and Parallelism * **Do This:** Use "Task.Parallel" or "Async.Parallel" for parallelizing independent computations. Consider using "System.Threading.Channels" for efficient producer-consumer patterns. * **Do This:** Leverage "Array.Parallel.map" and similar functions for parallel processing of arrays. * **Don't Do This:** Share mutable state between threads without proper synchronization. This inevitably leads to race conditions and data corruption. * **Why:** Modern CPUs have multiple cores; utilizing them can significantly improve performance. """fsharp // Example: Parallel processing with Async.Parallel open System let processItem (i: int) : Async<int> = async { printfn "Processing item %d on thread %d" i Thread.CurrentThread.ManagedThreadId do! Async.Sleep(100) // Simulate work return i * 2 } let items = [1..10] let parallelProcessing = items |> List.map processItem |> Async.Parallel |> Async.RunSynchronously printfn "Results: %A" parallelProcessing """ ### 2.4. Asynchronous Programming * **Do This:** Prefer "async" workflows over blocking operations, especially for I/O-bound tasks. * **Do This:** Use "Async.AwaitTask" or "task { ... }" for interoperating with .NET tasks. * **Don't Do This:** Block on asynchronous operations using "Async.RunSynchronously" unnecessarily. Try to keep asynchronous workflows "async all the way down." * **Why:** Asynchronous programming allows your application to remain responsive while waiting for long-running operations to complete. """fsharp // Example: Asynchronous file reading open System.IO let readFileAsync (filePath: string) : Async<string> = async { use reader = new StreamReader(filePath) let! content = reader.ReadToEndAsync() |> Async.AwaitTask return content } let processFile (filePath: string) = readFileAsync filePath |> Async.RunSynchronously |> printfn "%s" processFile "myFile.txt" """ ## 3. Coding Practices ### 3.1. Lazy Evaluation * **Do This:** Use sequences ("seq<'T>") for potentially large or infinite data streams to avoid unnecessary computation. * **Do This:** Use "Seq.cache" to cache the results of expensive sequence computations. * **Don't Do This:** Force evaluation of a sequence unless the results are immediately needed. * **Why:** Lazy evaluation defers computation until it is required, saving resources. """fsharp // Example: Using sequences for lazy evaluation let expensiveComputation (x: int) = printfn "Computing %d" x System.Threading.Thread.Sleep(100) // Simulate work x * x let numbers = seq { for i in 1..10 -> expensiveComputation i } // Only the first 3 items are computed let firstThree = numbers |> Seq.take 3 |> Seq.toList printfn "First three: %A" firstThree // Caching the sequence results. let cachedNumbers = numbers |> Seq.cache // the first time this is run, the sequence will execute cachedNumbers |> Seq.take 3 |> Seq.toList |> ignore // subsequent runs will be faster as the result is cached. cachedNumbers |> Seq.take 3 |> Seq.toList |> ignore """ ### 3.2. Memory Allocation * **Do This:** Use "Array" for fixed-size collections when frequent random access is required, and you need optimal performance. * **Do This:** Consider in-place mutation with mutable records or arrays for performance-critical sections, *but only after profiling and confirming a benefit.* Encapsulate mutable state tightly and document its use carefully. * **Don't Do This:** Unnecessary boxing/unboxing of value types. * **Why:** Reducing memory allocations and copies can improve performance and reduce GC pressure. """fsharp // Example: Using mutable records for performance (use with caution!) type MutablePoint = { mutable X: int; mutable Y: int } let movePoint (p: MutablePoint) (dx: int) (dy: int) = p.X <- p.X + dx p.Y <- p.Y + dy let mutablePoint = { X = 10; Y = 20 } movePoint mutablePoint 5 5 printfn "Mutable point: %A" mutablePoint // Example with array mutation: let arr = [| 1; 2; 3; 4; 5 |] for i in 0..arr.Length - 1 do arr.[i] <- arr.[i] * 2 printfn "%A" arr """ ### 3.3. Pattern Matching * **Do This:** Ensure that pattern matching is exhaustive to avoid runtime exceptions. * **Do This:** Order patterns from most specific to least specific for optimal matching performance. Especially with Discriminated unions (DUs) * **Don't Do This:** Nested "if-then-else" statements instead of pattern matching. * **Why:** Efficient pattern matching is a powerful feature of F#. """fsharp // Example: Efficient pattern matching with DUs type Result<'T> = | Success of 'T | Failure of string let processResult (result: Result<int>) = match result with | Success value -> printfn "Success: %d" value | Failure message -> printfn "Failure: %s" message """ ### 3.4. String Manipulation * **Do This:** Use "System.Text.StringBuilder" for building strings in loops or when concatenating many strings. * **Don't Do This:** Repeatedly use the "+" operator to concatenate strings, as this creates new string objects on each iteration. * **Why:** String concatenation with "+" creates new string objects, which can be inefficient. """fsharp // Example: Using StringBuilder for efficient string concatenation open System.Text let buildString (count: int) = let sb = StringBuilder() for i in 1..count do sb.Append("item ") |> ignore sb.Append(i.ToString()) |> ignore sb.AppendLine() |> ignore sb.ToString() let result = buildString 1000 printfn "%s" result """ ### 3.5. Boxing and Unboxing * **Do This:** Be aware of boxing and unboxing operations, which can degrade performance. Avoid them whenever possible. Use generics to avoid boxing. * **Don't Do This:** Pass value types (structs, enums, primitive types) to functions expecting "obj" unless necessary. * **Why:** Boxing and unboxing involve allocating memory on the heap and can be costly. """fsharp // Example: Avoiding boxing with generics let printValue<'T> (value: 'T) = printfn "%A" value // No boxing occurs because 'int' is a generic parameter, not 'obj' printValue 10 // Using a non generic context with a boxed result let printObjectUnoptimized (obj: obj) = printfn "%A" obj let someInteger : int = 420 printObjectUnoptimized (box someInteger) """ ## 4. Tooling and Analysis ### 4.1. Profiling * **Do This:** Use profiling tools (e.g., .NET Performance Monitor, Visual Studio Profiler, PerfView, JetBrains dotTrace) to identify performance bottlenecks. * **Don't Do This:** Guess where performance problems lie. Always measure. * **Why:** Profiling provides concrete data on where time is spent in your application. ### 4.2. Benchmarking * **Do This:** Use benchmarking libraries (e.g., BenchmarkDotNet) to compare the performance of different implementations. * **Don't Do This:** Rely on anecdotal evidence or intuition about performance. * **Why:** Benchmarking provides accurate measurements of execution time and memory usage. ### 4.3. F# Compiler Options * **Do This:** Explore compiler optimization options (e.g., "/optimize+", "/platform:x64") to improve code generation. * **Why:** The F# compiler can perform various optimizations to improve performance. * **Use:** Research and apply the latest performance related compiler flags based on the F# version you are using. Check release notes to ensure you are not using deprecated features. ## 5. Interoperability with .NET ### 5.1. Leveraging .NET Libraries * **Do This:** Utilize highly optimized .NET libraries (e.g., "System.Numerics", "System.Collections.Immutable", "System.Memory") for performance-critical tasks. Especially for vectorization and low-level memory manipulation. * **Don't Do This:** Re-implement functionality that is already available and well-optimized in .NET. * **Why:** .NET provides a wealth of optimized libraries that can improve performance. """fsharp // Example: Using System.Numerics for vector operations open System.Numerics let vectorAddition (v1: Vector3) (v2: Vector3) = v1 + v2 let result = vectorAddition (Vector3(1.0f, 2.0f, 3.0f)) (Vector3(4.0f, 5.0f, 6.0f)) printfn "%A" result """ ### 5.2. Calling C# Code * **Do This:** If necessary, call into performance-critical C# code, especially when dealing with existing libraries or algorithms. Ensure the C# code is also written with high performance in mind. * **Don't Do This:** Mix F# and C# code unnecessarily. Only use C# for specific performance bottlenecks. * **Why:** Sometimes, existing C# libraries or hand-optimized C# code can provide better performance than equivalent F# implementations. ## 6. Specific Scenarios and Optimizations ### 6.1. Web Development * **Do This:** Utilize caching strategies (e.g., in-memory caching, distributed caching) to reduce database load and improve response times. * **Do This:** Optimize database queries and use appropriate indexing. * **Do This:** Compress responses (e.g., using Gzip) to reduce network bandwidth usage. * **Why:** Web applications often face high traffic loads, making performance optimization crucial. ### 6.2. Data Processing * **Do This:** Use "Array.Parallel.map", "Seq.Parallel.chunkBySize" or Task/Async based parallelism for parallelizing data processing tasks. * **Do This:** Consider using specialized data processing libraries like Deedle or Math.NET Numerics. * **Why:** Data processing tasks often involve large datasets, making parallelization and efficient data structures essential. ### 6.3. Game Development * **Do This:** Use structs instead of records or classes for frequently updated game objects to reduce GC pressure. Employ "Unsafe" operations *very* judiciously where proven to provide a significant gain and only after careful consideration of safety. * **Do This:** Optimize rendering loops and minimize draw calls. * **Why:** Game development requires real-time performance, making every optimization crucial. ## 7. Conclusion Adhering to these performance optimization standards will help you write efficient and responsive F# applications. Remember to always profile and benchmark your code to identify bottlenecks and measure the impact of your optimizations. Choosing the right architecture, applying appropriate coding practices, and leveraging the tooling and libraries available in the .NET ecosystem are all essential for achieving optimal performance. Remember that performance is not the *only* consideration - maintainability, readability and security are also vital aspects of software engineering. Only optimize after profiling, and always weigh the performance gains against the complexity introduced.