Dive C2.01.2
Instance methods → functions
An object with state, and a method that changes it — translated to a record and a returned copy.
A C# instance method has two things a static helper does not: an implicit
this — the object it was called on — and, often, the power to
mutate that object's fields. account.Deposit(50m) reaches into
this.Balance and reassigns it. F# splits the method back into its parts: the data
becomes a record, and the method becomes a function in a module that takes the record as an
ordinary argument. The implicit this is now explicit, and because records are
immutable, the function does not change the record it is given — it returns a new one.
This dive builds an account both ways and shows the consequence: after a deposit, the original record still
holds its old balance.
this becomes an argument
The C# class binds the field Balance and the method Deposit
together. Inside the method, this is implicit: Balance += amt
means this.Balance = this.Balance + amt. The call site is
account.Deposit(50m), and it changes account in place.
The F# form names the same parts separately. type Account = { Balance: decimal } is
the data. module Account holds deposit, a function whose last
parameter is the account itself. The body uses a copy-and-update expression,
{ acc with Balance = … }, which produces a new record with one field changed and the
rest copied. The signature says it plainly: decimal -> Account -> Account —
give it an amount and an account, get an account back.
C#
class Account { public decimal Balance; public void Deposit(decimal amt) => Balance += amt; // mutates this.Balance } // account.Deposit(50m) changes account in place
F#
type Account = { Balance: decimal } module Account = let deposit amount acc = { acc with Balance = acc.Balance + amount } // val deposit : decimal -> Account -> Account
this is F#'s
explicit acc argument, and the in-place Balance += amt becomes
a copy-and-update that returns a fresh record. Read the signature and the whole behaviour is on the page: it
consumes an Account and produces an Account.The original is unchanged
Run the F# at the REPL and the difference from C# is concrete. A starting account
acc0 holds 100M. Depositing 50m
yields a value that holds 150M — and acc0 still holds
100M. No call reached back and edited the original, because there is no mutation to
do it with: deposit returned a separate record.
let acc0 = { Balance = 100m } // val acc0 : Account = { Balance = 100M } Account.deposit 50m acc0 // val it : Account = { Balance = 150M } acc0 // val it : Account = { Balance = 100M } (unchanged)
- { Balance: decimal }
- a record type — the data the class used to hold, on its own.
- { acc with Balance = … }
- copy-and-update: a new record, one field changed, the rest copied from
acc. - decimal -> Account -> Account
- the signature: an amount and an account in, a new account out.
- acc0 unchanged
- the original value still reads
100M— no method edited it.
The account argument comes last by convention. With currying and the pipe,
acc0 |> Account.deposit 50m reads as "take acc0, deposit 50"
— the data-last shape that lets account operations chain. The pipe and currying are covered in C1.03 and
C1.04.
A ledger over the immutable record
Apply a sequence of steps to the starting account { Balance = 100m }. Each step
calls deposit or withdraw and produces a
new record; the previous one keeps its value. The readout shows every value in the chain,
the last one being the result, and confirms the start is still 100M.
deposit / withdraw · each step a new record
| sequence | result | start after |
|---|---|---|
| deposit 50m | { Balance = 150M } | { Balance = 100M } |
| deposit 50m, withdraw 30m | { Balance = 120M } | { Balance = 100M } |
| withdraw 40m, deposit 10m | { Balance = 70M } | { Balance = 100M } |
A "mutating" call is a returned copy. The chain of balances is a chain of distinct records, and the one you started from is still right there, holding its first value.
The C# habit
A method reads this and writes its fields; the call changes the object in place.
F# today
A function takes the record as an argument and returns a new one; { acc with … } copies, never edits.
Where this leads
Functions in a module are the default home for behaviour, but F# does keep dot-syntax for the cases that
read better with it — vec.Length rather than Vec.length vec.
Those are type members, written on the type itself. The next dive covers them alongside the
other organisational rules a C# developer meets — namespaces, access control, and the one that catches
everyone out: F# files are read in order.
References
Sources
- Microsoft — F# records — record types and the copy-and-update
{ x with … }expression. - Microsoft — F# modules — functions over a record, grouped in a module.
- The F# Language Specification — records, immutability, and copy-and-update.
Related in this course
- C2.01.3 — Namespaces, members & file order — type members and the organisational rules.
- C2.01.1 — Static classes → modules — the stateless case this builds on.
- C1.03 — Functions — currying and the pipe that make data-last operations chain.