Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
F# is a functional-first programming language that runs on .NET. It combines functional programming with object-oriented and imperative programming paradigms, offering strong type inference, data immutability, and pattern matching.
F#, despite being a well-designed functional language, has several common anti-patterns that can lead to code that’s difficult to maintain, inefficient, or doesn’t follow the language’s idioms. Here are the most important anti-patterns to avoid when writing F# code.
// Anti-pattern: Excessive mutation
let processItems items =
let mutable sum = 0
let mutable count = 0
for item in items do
sum <- sum + item
count <- count + 1
if count > 0 then sum / count else 0
// Better approach: Use functional constructs
let processItems items =
let items = Seq.toList items
match items with
| [] -> 0
| _ -> items |> List.sum |> fun sum -> sum / List.length items
// Even better
let processItems items =
items
|> Seq.fold (fun (sum, count) item -> (sum + item, count + 1)) (0, 0)
|> function
| (_, 0) -> 0
| (sum, count) -> sum / count
Avoid excessive use of mutable variables and imperative loops. F# is a functional-first language, so prefer immutable data and functional constructs like pattern matching, recursion, and higher-order functions.
// Anti-pattern: Not leveraging the type system
type User = {
Name: string
Email: string
Status: string // Could be "active", "inactive", "pending"
}
let isUserActive user =
user.Status = "active"
// Better approach: Use discriminated unions
type UserStatus =
| Active
| Inactive
| Pending
type User = {
Name: string
Email: string
Status: UserStatus
}
let isUserActive user =
match user.Status with
| Active -> true
| _ -> false
Leverage F#‘s powerful type system, especially discriminated unions, to model your domain accurately. This provides compile-time safety and makes your code more expressive and self-documenting.
// Anti-pattern: Ignoring option types
let findUser (users: User list) (id: string) =
users |> List.find (fun u -> u.Id = id) // Throws if not found
// Usage that might throw
let user = findUser users "123"
printfn "Found user: %s" user.Name
// Better approach: Use option types
let findUser (users: User list) (id: string) =
users |> List.tryFind (fun u -> u.Id = id)
// Safe usage with pattern matching
match findUser users "123" with
| Some user -> printfn "Found user: %s" user.Name
| None -> printfn "User not found"
// Or using Option.map and defaultValue
findUser users "123"
|> Option.map (fun user -> user.Name)
|> Option.defaultValue "Unknown user"
|> printfn "User: %s"
Use option types ('T option
) to represent values that might not exist, rather than throwing exceptions or using null. This makes the possibility of missing values explicit in your type signatures and forces clients to handle both cases.
// Anti-pattern: Excessive OO style
type Customer() =
let mutable name = ""
let mutable email = ""
member this.Name
with get() = name
and set(value) = name <- value
member this.Email
with get() = email
and set(value) = email <- value
member this.IsValid() =
not (String.IsNullOrEmpty(name)) && email.Contains("@")
// Usage
let customer = Customer()
customer.Name <- "John"
customer.Email <- "john@example.com"
if customer.IsValid() then
// Do something
// Better approach: Use immutable records
type Customer = {
Name: string
Email: string
}
let isValid customer =
not (String.IsNullOrEmpty(customer.Name)) && customer.Email.Contains("@")
// Usage
let customer = { Name = "John"; Email = "john@example.com" }
if isValid customer then
// Do something
Avoid excessive object-oriented style with mutable classes and properties. Instead, prefer immutable records and standalone functions. This leads to code that’s easier to reason about and test.
// Anti-pattern: Not using pattern matching
let describe x =
if x = 0 then
"Zero"
else if x > 0 then
"Positive"
else
"Negative"
let processShape shape =
if shape.GetType() = typeof<Circle> then
let circle = shape :?> Circle
Math.PI * circle.Radius * circle.Radius
else if shape.GetType() = typeof<Rectangle> then
let rect = shape :?> Rectangle
rect.Width * rect.Height
else
0.0
// Better approach: Use pattern matching
let describe x =
match x with
| 0 -> "Zero"
| x when x > 0 -> "Positive"
| _ -> "Negative"
// With discriminated unions
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base': float * height: float
let area shape =
match shape with
| Circle radius -> Math.PI * radius * radius
| Rectangle(width, height) -> width * height
| Triangle(base', height) -> base' * height / 2.0
Use pattern matching extensively, especially with discriminated unions. Pattern matching makes your code more concise, readable, and helps ensure you handle all possible cases.
// Anti-pattern: Excessive type annotations
let add (x: int) (y: int) : int =
x + y
let process (items: int list) : float =
let sum: int = List.sum items
let count: int = List.length items
float sum / float count
// Better approach: Let type inference work
let add x y = x + y
let process items =
let sum = List.sum items
let count = List.length items
float sum / float count
Avoid excessive type annotations. F# has powerful type inference, so you usually don’t need to specify types explicitly. Add type annotations only when necessary for clarity, to resolve ambiguities, or at API boundaries.
// Anti-pattern: Not using the pipe operator
let result = List.map (fun x -> x * 2) (List.filter (fun x -> x % 2 = 0) [1..10])
// Better approach: Use the pipe operator
let result =
[1..10]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * 2)
Use the pipe operator (|>
) to create readable data processing pipelines. The pipe operator makes the flow of data through transformations clear and reduces the need for nested function calls or intermediate variables.
// Anti-pattern: Ignoring partial application
let filter predicate list =
List.filter predicate list
let map mapper list =
List.map mapper list
// Better approach: Leverage partial application
let filter predicate =
List.filter predicate
let map mapper =
List.map mapper
// Usage
let evenNumbers =
[1..10]
|> filter (fun x -> x % 2 = 0)
|> map (fun x -> x * 2)
Leverage partial application to create specialized functions from more general ones. This allows for more concise and composable code.
// Anti-pattern: Complex pattern matching without active patterns
let categorizeNumber x =
match x with
| x when x = 0 -> "Zero"
| x when x > 0 && x % 2 = 0 -> "Positive even"
| x when x > 0 -> "Positive odd"
| x when x % 2 = 0 -> "Negative even"
| _ -> "Negative odd"
// Better approach: Use active patterns
let (|Zero|Positive|Negative|) x =
if x = 0 then Zero
elif x > 0 then Positive x
else Negative x
let (|Even|Odd|) x =
if x % 2 = 0 then Even x else Odd x
let categorizeNumber x =
match x with
| Zero -> "Zero"
| Positive(Even _) -> "Positive even"
| Positive(Odd _) -> "Positive odd"
| Negative(Even _) -> "Negative even"
| Negative(Odd _) -> "Negative odd"
Use active patterns to encapsulate complex matching logic and make pattern matching more readable and maintainable. Active patterns allow you to abstract away the details of how values are matched.
// Anti-pattern: Not using units of measure
let distance = 5.0 // Is this meters, feet, or miles?
let time = 2.0 // Is this hours, minutes, or seconds?
let speed = distance / time
// Better approach: Use units of measure
[<Measure>] type m
[<Measure>] type s
[<Measure>] type h
let distance = 5.0<m>
let time = 2.0<s>
let speed = distance / time // Type is float<m/s>
// Convert to km/h
[<Measure>] type km
let kmPerHour = (speed * 3600.0<s/h>) * (1.0<km/1000.0<m>>)
Use units of measure to add physical units to your numeric types. This prevents mixing incompatible quantities and makes your code more self-documenting and less prone to errors.
// Anti-pattern: Nested monadic operations without computation expressions
let getUserData userId =
match tryFindUser userId with
| None -> Error "User not found"
| Some user ->
match tryGetUserPreferences user.Id with
| None -> Error "Preferences not found"
| Some prefs ->
match tryGetRecommendations prefs with
| None -> Error "No recommendations"
| Some recs -> Ok (user, prefs, recs)
// Better approach: Use computation expressions
let getUserData userId = result {
let! user = tryFindUser userId
let! prefs = tryGetUserPreferences user.Id
let! recs = tryGetRecommendations prefs
return (user, prefs, recs)
}
// Another example with async
let fetchData url = async {
let! response = httpClient.GetAsync(url) |> Async.AwaitTask
let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
return content
}
Use computation expressions (like async
, task
, result
, option
, etc.) to simplify working with monadic types. They make your code more readable by eliminating nested pattern matching and allowing a more imperative-like syntax for functional concepts.
// Anti-pattern: Manual data access code
let getCustomers() =
use connection = new SqlConnection(connectionString)
connection.Open()
use command = new SqlCommand("SELECT Id, Name, Email FROM Customers", connection)
use reader = command.ExecuteReader()
let customers = ResizeArray<Customer>()
while reader.Read() do
let customer = {
Id = reader.GetInt32(0)
Name = reader.GetString(1)
Email = reader.GetString(2)
}
customers.Add(customer)
customers |> Seq.toList
// Better approach: Use type providers
type Sql = SqlDataProvider<...> // Configuration omitted for brevity
let getCustomers() =
use context = Sql.GetDataContext()
query {
for customer in context.Customers do
select {
Id = customer.Id
Name = customer.Name
Email = customer.Email
}
}
|> Seq.toList
Use type providers to access external data sources like databases, web services, or file formats. Type providers generate types based on the schema of the data source, providing compile-time checking and IntelliSense support.
// Anti-pattern: Recursion without tail calls
let rec factorial n =
if n <= 1 then 1
else n * factorial (n - 1) // Not tail-recursive
// Better approach: Use tail recursion
let factorial n =
let rec loop n acc =
if n <= 1 then acc
else loop (n - 1) (n * acc) // Tail-recursive
loop n 1
When using recursion, make your functions tail-recursive to avoid stack overflow errors with large inputs. In a tail-recursive function, the recursive call is the last operation in the function.
// Anti-pattern: Everything in one file without organization
// MyApp.fs
module MyApp
// Types, functions, and values all mixed together
// Better approach: Organize code into modules and namespaces
// Domain.fs
namespace MyApp.Domain
type User = { /* ... */ }
type Order = { /* ... */ }
// Services.fs
namespace MyApp.Services
open MyApp.Domain
module UserService =
let findUser userId = // ...
let updateUser user = // ...
module OrderService =
let placeOrder userId items = // ...
let cancelOrder orderId = // ...
Organize your code into modules and namespaces based on functionality. This improves maintainability, compilation times, and allows for better encapsulation of implementation details.
// Anti-pattern: Exception-based error handling
let validateEmail email =
if String.IsNullOrEmpty(email) then
raise (ArgumentException("Email cannot be empty"))
elif not (email.Contains("@")) then
raise (ArgumentException("Email must contain @"))
else
email
let validatePassword password =
if String.IsNullOrEmpty(password) then
raise (ArgumentException("Password cannot be empty"))
elif password.Length < 8 then
raise (ArgumentException("Password must be at least 8 characters"))
else
password
// Better approach: Railway-oriented programming
type ValidationResult<'T> = Result<'T, string>
let validateEmail email : ValidationResult<string> =
if String.IsNullOrEmpty(email) then
Error "Email cannot be empty"
elif not (email.Contains("@")) then
Error "Email must contain @"
else
Ok email
let validatePassword password : ValidationResult<string> =
if String.IsNullOrEmpty(password) then
Error "Password cannot be empty"
elif password.Length < 8 then
Error "Password must be at least 8 characters"
else
Ok password
// Compose validations
let validateCredentials email password =
result {
let! validEmail = validateEmail email
let! validPassword = validatePassword password
return (validEmail, validPassword)
}
Use railway-oriented programming (using Result<'T, 'Error>
or similar types) for error handling instead of exceptions. This makes error paths explicit in your code and allows for better composition of functions that might fail.
// Anti-pattern: Blocking async code
let fetchData url =
async {
let! response = httpClient.GetAsync(url) |> Async.AwaitTask
return response
} |> Async.RunSynchronously // Blocks the thread
// Better approach: Keep async workflows async
let fetchData url =
async {
let! response = httpClient.GetAsync(url) |> Async.AwaitTask
return response
}
// Usage
let processDataAsync() = async {
let! data = fetchData "https://example.com/api/data"
let! moreData = fetchData "https://example.com/api/more-data"
return processResults data moreData
}
// Only at the top level or entry point
let main args =
processDataAsync() |> Async.RunSynchronously
Use Async
and Task
properly for asynchronous operations. Don’t block async workflows with Async.RunSynchronously
except at the top level of your application. Compose async operations using computation expressions.
// Anti-pattern: Using mutable collections
let processItems items =
let results = ResizeArray<int>()
for item in items do
if item % 2 = 0 then
results.Add(item * 2)
results |> Seq.toList
// Better approach: Use immutable collections and transformations
let processItems items =
items
|> List.filter (fun item -> item % 2 = 0)
|> List.map (fun item -> item * 2)
Prefer immutable collections (list
, seq
, array
, etc.) and functional transformations over mutable collections and in-place modifications. This leads to code that’s easier to reason about and less prone to bugs.