F# code blocks
last modified May 17, 2025
F# uses indentation-based code blocks to organize code structure. Unlike languages that use braces, F# determines block scope through consistent indentation. This creates clean, readable code with visual structure.
Basic code blocks
Code blocks in F# are created by indenting under constructs like functions, loops, and conditionals. The standard convention is 4 spaces per level.
// Function with a code block
let calculateTotal (price: float) (quantity: int) =
let subtotal = price * float quantity
let tax = subtotal * 0.08
subtotal + tax // Last expression is return value
printfn "Total: %.2f" (calculateTotal 25.99 3)
// Conditional with blocks
let checkNumber n =
if n > 0 then
printfn "Positive number"
printfn "Double: %d" (n * 2)
else
printfn "Non-positive number"
printfn "Absolute: %d" (abs n)
checkNumber 5
checkNumber -3
This shows basic code blocks in functions and conditionals. All statements in a block must align at the same indentation level. The block ends when the indentation returns to the previous level.
λ dotnet fsi basic_blocks.fsx Total: 84.21 Positive number Double: 10 Non-positive number Absolute: 3
Nested blocks
Blocks can be nested by adding additional indentation levels. Each nested block increases the indentation, creating a clear visual hierarchy.
let analyzeNumbers numbers =
if List.isEmpty numbers then
printfn "No numbers to analyze"
else
printfn "Number count: %d" (List.length numbers)
let evens, odds =
numbers
|> List.partition (fun x -> x % 2 = 0)
printfn "Even numbers: %A" evens
printfn "Odd numbers: %A" odds
if not (List.isEmpty evens) then
printfn "Even stats:"
printfn " Min: %d" (List.min evens)
printfn " Max: %d" (List.max evens)
if not (List.isEmpty odds) then
printfn "Odd stats:"
printfn " Min: %d" (List.min odds)
printfn " Max: %d" (List.max odds)
analyzeNumbers [1..10]
This example shows multiple levels of nested blocks. The outer function block contains conditional blocks, which themselves contain additional blocks. Each level is indented further than its parent.
λ dotnet fsi nested_blocks.fsx Number count: 10 Even numbers: [2; 4; 6; 8; 10] Odd numbers: [1; 3; 5; 7; 9] Even stats: Min: 2 Max: 10 Odd stats: Min: 1 Max: 9
Let bindings in blocks
Let bindings can create local scopes within blocks. These bindings are only visible within their containing block, helping organize complex logic.
type User = { Name: string; Age: int }
let processUser user =
let isValid =
not (System.String.IsNullOrWhiteSpace user.Name) &&
user.Age > 0
if isValid then
let greeting =
if user.Age >= 18 then "Hello, " + user.Name
else "Hi, " + user.Name + " (minor)"
printfn "%s" greeting
let accountType =
match user.Age with
| a when a >= 65 -> "Senior"
| a when a >= 18 -> "Adult"
| _ -> "Junior"
printfn "Account type: %s" accountType
else
printfn "Invalid user data"
processUser { Name = "Alice"; Age = 30 }
processUser { Name = ""; Age = 15 }
This demonstrates local let bindings within blocks. Each binding is only available in its scope. The greeting and accountType bindings are only accessible within the isValid true block.
λ dotnet fsi let_blocks.fsx Hello, Alice Account type: Adult Invalid user data
Expression blocks
F# allows creating ad-hoc expression blocks to organize code or limit scope. These blocks evaluate to their last expression and can appear anywhere.
let calculateComplexValue x y =
let intermediate =
let a = x * x
let b = y * y
a + b // This value becomes 'intermediate'
let result =
if intermediate > 100 then
let scaled = intermediate / 10
scaled + 5
else
intermediate * 2
result
printfn "Result: %d" (calculateComplexValue 5 8)
// Standalone expression block
let randomValue =
let r = System.Random()
let a = r.Next(10)
let b = r.Next(10)
a + b
printfn "Random sum: %d" randomValue
These examples show expression blocks used within functions and as standalone values. Each block creates its own scope and evaluates to its last expression.
λ dotnet fsi expression_blocks.fsx Result: 13 Random sum: 11
Module and namespace blocks
Larger code organization uses module and namespace blocks. These create named scopes for related functionality and follow the same indentation rules.
namespace MathOperations
module Arithmetic =
let add x y = x + y
let subtract x y = x - y
module Advanced =
let multiply x y = x * y
let divide x y = x / y
module Geometry =
let circleArea radius =
System.Math.PI * radius * radius
let rectangleArea width height =
width * height
// Usage from another file would be:
// open MathOperations.Arithmetic
// printfn "%d" (add 5 3)
This shows namespace and module blocks. The Arithmetic module contains an inner Advanced module. All contents are indented under their respective module declarations.
Special block forms
F# has several special block forms for specific purposes, like computation expressions and class definitions, which follow similar indentation rules.
// Computation expression block
let safeDivide x y =
match y with
| 0 -> None
| _ -> Some (x / y)
printfn "Safe divide: %A" (safeDivide 10 2)
printfn "Safe divide: %A" (safeDivide 10 0)
// Class definition block
type Person(name: string, age: int) =
member _.Name = name
member _.Age = age
member _.Greet() =
printfn "Hello, my name is %s" name
member _.IsAdult =
age >= 18
let p = Person("Alice", 30)
p.Greet()
These examples demonstrate special block forms. The option computation expression and Person class both use indented blocks to define their contents, following F#'s standard indentation rules.
λ dotnet fsi special_blocks.fsx Safe divide: Some 5 Hello, my name is Alice
Best practices
Consistent indentation is crucial in F#. Follow these practices for clean, maintainable code blocks.
// Good practices example
module CleanCode =
// Use consistent 4-space indents
let calculate x y =
let intermediate =
let a = x * 2
let b = y * 3
a + b
if intermediate > 100 then
printfn "Large result"
intermediate / 10
else
printfn "Normal result"
intermediate
// Keep blocks focused
let processData data =
let cleanData =
data
|> List.filter (fun x -> x > 0)
|> List.map (fun x -> x * 2)
let sum = List.sum cleanData
let avg = float sum / float (List.length cleanData)
(sum, avg)
// Avoid mixing tabs and spaces
// Avoid inconsistent indentation
// Keep block sizes reasonable
This example demonstrates good practices: consistent 4-space indents, focused blocks, and clean organization. The module shows well-structured F# code.
F# code blocks provide a clean, visually apparent way to organize code through indentation. By understanding block structure and following consistent practices, you can write F# code that's both elegant and maintainable. The indentation-based approach reduces clutter while making scope and structure immediately visible.