Skip to content
AgileLogics
7 min read

Classes and Members

Prerequisites: Records

F# is functional-first, but it has full support for .NET classes. Use them when you need encapsulated mutable state, when .NET APIs expect them, or when writing code for C# callers.

type BankAccount(owner: string, initialBalance: decimal) =
    let mutable balance = initialBalance

    member _.Owner = owner
    member _.Balance = balance

    member _.Deposit(amount: decimal) =
        if amount <= 0m then invalidArg (nameof amount) "must be positive"
        balance <- balance + amount

    member this.Withdraw(amount: decimal) =
        if amount > this.Balance then false
        else
            balance <- balance - amount
            true

    override _.ToString() = $"{owner}: {balance}"

let account = BankAccount("Ada", 100m)
account.Deposit 50m
printfn "%b" (account.Withdraw 30m)
printfn "%b" (account.Withdraw 500m)
printfn "%M" account.Balance
printfn "%s" (string account)
true
false
120
Ada: 120
  • The parameters after the type name form the primary constructor. They're available everywhere inside the class.
  • let inside a class defines private fields and helpers.
  • member defines public properties and methods. this (or _, when unused) names the current instance — you can pick any identifier.
  • override replaces an inherited method like ToString.
  • Methods usually take tupled arguments, like C#: Withdraw(amount).

Properties

type Thermostat() =
    let mutable target = 20.0

    member val Name = "Living room" with get, set

    member _.Target
        with get () = target
        and set value = target <- max 5.0 (min 30.0 value)

    member this.Describe() = $"{this.Name}: {this.Target} C"

let t = Thermostat()
t.Target <- 42.0
t.Name <- "Kitchen"
printfn "%s" (t.Describe())
Kitchen: 30 C

member val ... with get, set creates an auto-property; the explicit with get () = ... and set value = ... form lets you validate.

Static members and extra constructors

type Vector(x: float, y: float) =
    member _.X = x
    member _.Y = y
    member _.Length = sqrt (x * x + y * y)

    new(size: float) = Vector(size, size)

    static member Zero = Vector(0.0, 0.0)
    static member (+) (a: Vector, b: Vector) = Vector(a.X + b.X, a.Y + b.Y)

    override _.ToString() = $"({x}, {y})"

let v = Vector(3.0, 4.0) + Vector(1.0)
printfn "%O %.2f" v v.Length
printfn "%O" Vector.Zero
(4, 5) 6.40
(0, 0)

static member (+) overloads an operator. %O formats a value with its ToString() method.

Named and optional arguments

type Greeter() =
    member _.Greet(name: string, ?greeting: string, ?excited: bool) =
        let greeting = defaultArg greeting "Hello"
        let ending = if defaultArg excited false then "!" else "."
        $"{greeting}, {name}{ending}"

let g = Greeter()
printfn "%s" (g.Greet "Ada")
printfn "%s" (g.Greet("Ada", "Hi"))
printfn "%s" (g.Greet("Ada", excited = true))
Hello, Ada.
Hi, Ada.
Hello, Ada!

Optional parameters (?name) are only available on methods, not on let functions — another reason classes are handy for public APIs.

Records or classes?

  • Records for data: immutable, structural equality, pattern matching.
  • Classes for behavior with hidden state: a connection pool, a cache, a game engine object, or anything a .NET framework asks you to subclass.