F# Discriminated Unions & Their Benefits
[F#, .NET]
Suppose we wish to model the following in F#:
- A
CurrentAccount
- this has:- Account number
- Balance
- A
SavingsAccount
- this has:- Account number
- Balance
- InterestRate
- Minimum Balance
- A
MobileMoneyAccount
- this has:- Mobile number
- Balance
We would do it as follows:
type CurrentAccount = {
AccountNumber: string
Balance: decimal
}
type SavingsAccount = {
AccountNumber: string
Balance: decimal
InterestRate: decimal
MinimumBalance: decimal
}
type MobileMoneyAccount = {
MobilePhoneNumber: string
Balance: decimal
}
Let us then proceed to create a collection of our accounts:
let accounts = [myCurrentAccount, mySavingsAccount, myMobileMoneyAccount]
Now comes the problem - iterate through each of the accounts and print their details.
Two solutions present themselves:
- Treat each element as an
object
and cast repeatedly until you get the right type. - Introduce a base
interface
, and cast to this.
There is a third, more natural, and elegant solution, courtesy of functional programming: discriminated unions.
The premise is as follows:
MobileMoneyAccount
,SavingsAccount
, andCurrentAccount
are ultimately types ofAccount
.- An
Account
can thus be defined as either of these. - The types that an
Account
can contain do not have to have anything in common, unlike inheritance.
We define it as follows:
type Account =
| Current of CurrentAccount
| Savings of SavingsAccount
| MobileMoney of MobileMoneyAccount
We are saying that either of these can represent an Account
.
And, since they do not need to have anything in common, we can safely do this (if we wanted to).
type Account =
| Current of CurrentAccount
| Savings of SavingsAccount
| MobileMoney of MobileMoneyAccount
| Number of string
| PIN of int
But back to our original solution:
type Account =
| Current of CurrentAccount
| Savings of SavingsAccount
| MobileMoney of MobileMoneyAccount
We then define a list of Account
, populating it with our accounts:
// Create a list of Account
let accounts = [
Current myCurrentAccount
Savings mySavingsAccount
MobileMoney myMobileMoneyAccount
]
If we needed to iterate through our collection, we can now safely do so and use pattern matching (via match
) to execute our logic conditionally:
// Iterate and print details
accounts |> List.iter (fun account ->
match account with
| Current acc -> printfn "Current Account: %s, Balance: %s" acc.AccountNumber (acc.Balance.ToString("#,0"))
| Savings acc -> printfn "Savings Account: %s, Rate: %M%%, Balance: %s" acc.AccountNumber acc.InterestRate (acc.Balance.ToString("#,0"))
| MobileMoney acc -> printfn "Mobile Money Account: Phone Number: %s, Balance: %s" acc.MobilePhoneNumber (acc.Balance.ToString("#,0"))
)
This will print the following:
Current Account: 23423434, Balance: 10,000
Savings Account: 23424234234, Rate: 10%, Balance: 10,000
Mobile Money Account: Phone Number: +257721212313, Balance: 10,000
Note that in the code, the match forces you to handle each of the possible options.
If we try to delete the code handling MobileMoneyAccount
, the compiler will complain:
This means you must provide logic for all branches, eliminating a whole swathe of logic errors.
TLDR
Discriminated unions allow you to write code that allows you to define complex types that can be made up of alternative types.
The code is in my GitHub.
Happy hacking!