jonnifyfsharp

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.

C2.01.2 · part 1

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
The hidden becomes visible. C#'s implicit 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.
C2.01.2 · part 2

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

each arrow is a function call returning a new Account { Balance = 100M } deposit 50 { Balance = 150M } original, unchanged
start = { Balance = 100M } · deposit 50m => { Balance = 150M } · start still { Balance = 100M }
Without JavaScript: the three sequences, computed by hand (start = 100M)
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

Related in this course