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.