ZetCode

F# unit type

last modified May 17, 2025

The unit type in F# is a special type that has exactly one value, written (). It serves as a placeholder when no meaningful value needs to be returned or passed, similar to void in C-like languages but with more consistent behavior in F#'s type system. Unlike void, which represents the absence of a value, unit is an actual type with a concrete value, making function signatures more predictable and enabling better composition in functional programming.

The unit type is frequently used in functions that perform side effects, such as logging or printing, without requiring a return value. Since every function in F# must return something, unit ensures that functions producing effects still conform to the type system. This also simplifies chaining operations, as unit-returning functions remain composable within pipelines without breaking functional flow.

Understanding the unit type

The unit type represents the absence of a specific value. It's used primarily in two situations: when a function doesn't return any meaningful value, and when a function doesn't take any meaningful parameters (though the latter is less common in F#).

basic_unit.fsx
// Function that returns unit
let printHello () =
    printfn "Hello, F#!"
    // No return statement needed - unit is implied

// Calling the function
printHello ()

// Explicit unit annotation
let doNothing : unit = ()

// Function with unit parameter and return
let logMessage (msg: string) : unit =
    printfn "LOG: %s" msg

logMessage "This is a test message"

This example shows basic usage of the unit type. The printHello function implicitly returns unit, while doNothing explicitly holds the unit value. The printfn function also returns unit.

λ dotnet fsi basic_unit.fsx
Hello, F#!
LOG: This is a test message
hello

Unit in function signatures

Functions that perform side effects but don't return meaningful values typically return unit. The unit type ensures these functions integrate properly with F#'s type system and functional programming patterns.

function_signatures.fsx
// Function with unit parameter
let initialize () =
    printfn "Initializing system..."
    // Initialization code here

// Function that takes unit and returns unit
let rec countdown (n: int) : unit =
    if n > 0 then
        printfn "%d..." n
        countdown (n - 1)
    else
        printfn "Liftoff!"

// Calling functions with unit
initialize ()
countdown 5

// Unit in higher-order functions
let executeThreeTimes (action: unit -> unit) =
    action ()
    action ()
    action ()

let beep () = printfn "\a" // System beep
executeThreeTimes beep

This code demonstrates unit in various function signatures. initialize takes unit as a parameter, countdown returns unit, and executeThreeTimes takes a function that requires unit input and returns unit output.

λ dotnet fsi function_signatures.fsx
Initializing system...
5...
4...
3...
2...
1...
Liftoff!

Unit vs void

Unlike C#'s void which is truly nothing, F#'s unit is an actual type with a single value. This distinction allows unit to work consistently in all contexts where types are expected.

unit_vs_void.fsx
// In F#, even "void" functions return a value
let result = printfn "This returns unit"
printfn "The result is: %A" result

// Unit can be stored in data structures
let unitList = [(); (); ()]
printfn "List of units: %A" unitList

// Unit works with generics
let unitOption : unit option = Some ()
printfn "Unit option: %A" unitOption

// Comparing with C# void
let csharpAction = new System.Action(fun () -> printfn "C# action")
let actionResult = csharpAction.Invoke()
printfn "C# action returns: %A" actionResult

This example highlights differences between F#'s unit and C#'s void. Unit can be stored in lists, options, and other data structures, while void cannot. The C# Action delegate returns void, which translates to unit in F#.

λ dotnet fsi unit_vs_void.fsx
This returns unit
The result is: ()
List of units: [(); (); ()]
Unit option: Some ()
C# action
C# action returns: ()

Unit in pattern matching

While pattern matching on unit isn't common (since there's only one possible value), it can be useful in some scenarios, particularly when working with generic code or interoperating with other .NET languages.

pattern_matching.fsx
let handleResult result =
    match result with
    | Some x -> printfn "Got value: %A" x
    | None -> printfn "Got nothing"

// Using unit with option
let maybeDoAction (shouldDo: bool) (action: unit -> unit) =
    if shouldDo then Some action else None

maybeDoAction true (fun () -> printfn "Performing action")
|> Option.iter (fun action -> action ())

// Unit in exhaustive matching
let describeUnit u =
    match u with
    | () -> "This is the one and only unit value"

printfn "%s" (describeUnit ())

This code shows unit appearing in pattern matching scenarios. The maybeDoAction function demonstrates how unit-returning functions can work with option types, and describeUnit shows the exhaustive pattern match for unit.

λ dotnet fsi pattern_matching.fsx
Performing action
This is the one and only unit value

Unit in asynchronous code

The unit type plays an important role in asynchronous workflows, where Async<unit> represents an asynchronous operation that completes without returning a meaningful value.

async_code.fsx
open System.Threading.Tasks

// Asynchronous function returning unit
let asyncOperation = async {
    do! Async.Sleep 1000
    printfn "Async operation completed"
}

// Running async unit operations
Async.Start asyncOperation

// Task-returning unit
let taskOperation () = Task.Run(fun () ->
    Task.Delay(500).Wait()
    printfn "Task operation completed"
)

taskOperation () |> ignore

// Combining async unit operations
let combined = async {
    printfn "Starting first operation"
    do! asyncOperation
    printfn "Starting second operation"
    do! asyncOperation
}

Async.RunSynchronously combined

This example demonstrates unit in asynchronous contexts. The async workflows use do! for operations that return unit, and we see how unit-returning tasks can be composed together.

λ dotnet fsi async_code.fsx
Starting first operation
Async operation completed
Starting second operation
Async operation completed
Async operation completed
Task operation completed

Unit and side effects

Functions that return unit typically perform side effects, as they don't return meaningful values. This serves as a useful marker in functional programming to identify code that affects external state.

side_effects.fsx
// Mutable state example
let counter =
    let count = ref 0
    fun () ->
        count.Value <- count.Value + 1
        printfn "Count is now: %d" count.Value

counter ()
counter ()
counter ()

// Unit-returning functions in pipelines
let processData data =
    data
    |> List.map (fun x -> x * 2)
    |> List.iter (printfn "Processed: %d")

processData [1..5]

// Unit as a marker for side effects
let pureAdd x y = x + y  // Pure function
let impureAdd x y =
    printfn "Adding %d and %d" x y  // Side effect
    x + y

printfn "Pure result: %d" (pureAdd 3 4)
printfn "Impure result: %d" (impureAdd 3 4)

This code illustrates how unit-returning functions often involve side effects. The counter function maintains mutable state, while impureAdd demonstrates how side effects can be mixed with pure computations.

λ dotnet fsi side_effects.fsx
Count is now: 1
Count is now: 2
Count is now: 3
Processed: 2
Processed: 4
Processed: 6
Processed: 8
Processed: 10
Pure result: 7
Adding 3 and 4
Impure result: 7

Unit in type parameters

The unit type can be used as a type parameter in generic types and functions, sometimes serving as a way to "ignore" one type parameter when another is needed.

type_parameters.fsx
// Generic function using unit
let createDefault<'T> () =
    printfn "Creating default instance of %s" typeof<'T>.Name
    Unchecked.defaultof<'T>

let intDefault = createDefault<int> ()
let unitDefault = createDefault<unit> ()
printfn "intDefault: %A, unitDefault: %A" intDefault unitDefault

// Unit in discriminated unions
type Result<'T, 'E> =
    | Success of 'T
    | Failure of 'E

let handleResult (result: Result<int, string>) =
    match result with
    | Success x -> printfn "Success: %d" x
    | Failure msg -> printfn "Error: %s" msg

// Using unit to indicate no error information
let performOperation x =
    if x > 0 then Success x
    else Failure "Invalid input"

let performVoidOperation () =
    printfn "Operation performed"
    Success ()

handleResult (performOperation 5)
handleResult (performOperation -2)

// Handler for Result<unit, string>
let handleVoidResult (result: Result<unit, string>) =
    match result with
    | Success () -> printfn "Success: ()"
    | Failure msg -> printfn "Error: %s" msg

handleVoidResult (performVoidOperation ())

This example shows unit appearing in generic contexts. createDefault demonstrates unit as a type parameter, while the Result type shows how unit can be used to indicate the absence of error information.

λ dotnet fsi type_parameters.fsx
Creating default instance of Int32
Creating default instance of Unit
intDefault: 0, unitDefault: ()
Success: 5
Error: Invalid input
Operation performed
Success: ()

The unit type in F# serves as a crucial part of the type system, representing the absence of a meaningful value while maintaining type safety. Unlike void in imperative languages, unit is a proper type that can be used in all contexts where types are expected. It plays important roles in function signatures, asynchronous programming, and marking side-effecting operations. Understanding unit is essential for writing correct F# code and properly interfacing with other .NET languages.

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.