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.
letinside a class defines private fields and helpers.memberdefines public properties and methods.this(or_, when unused) names the current instance — you can pick any identifier.overridereplaces an inherited method likeToString.- 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.