ZetCode

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.

basic_blocks.fsx
// 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.

nested_blocks.fsx
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.

let_blocks.fsx
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.

expression_blocks.fsx
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.

module_blocks.fsx
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.

special_blocks.fsx
// 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.

best_practices.fsx
// 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.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.