Basics 08 - Types (Part 1) - Type Aliasing, Records and Discriminated Unions
Time to discuss records and discriminated unions.
Learning-FSharp/Ch08-RecordsAndDiscriminatedUnions/Program.fs
Check out all the comments that are placed in the source file.
Type Aliasing
Type aliasing allows you to have your names for existing types. For instance:
type Email = string
type Width = uint
type Height = uint
type Rectangle = Width * Height
With type aliasing you can:
Have uniformity and consistent naming in the source
Change the type definition in one place instead of making changes across the source code
With type aliasing you cannot:
Make aliased type names available outside the world of F# (example: compiled in F#, not available in C#)
Achieve single-case discriminated union capability for domain modeling (this is discussed below). For instance,
type Email = string
, hereEmail
is just another name forstring
.Email
type won't solve the problems that exist when usingstring
for email addresses.
Records
Records allow the grouping of named values. Example:
type FullName = { First: string; Last: string }
Type
FullName
is a record typeIt has 2 fields
First
of type stringLast
of type string
Record types can be created by using other record types as field types:
type Student = { Name: FullName; Age: int; RollNo: string }
Construction and Updation
To create instances/values of records we use Record Expressions:
let fullName = { First = "John"; Last = "Doe" }
let student = { Name = { First = "John"; Last = "Doe" }; Age = 10; RollNo = "123456" }
Once created, fullName
and student
are fully immutable. You can't change fullName
or student
or value for any field.
We use Copy and Update Record Expressions to create a copy of an existing variable (or value) and supply new values for specific fields:
let newName = { fullName with First = "Jane" }
This creates a copy of fullName
with new value for First
.
Mutability of Fields
While it is possible to create record types with mutable fields, I highly recommend against it.
type FullNameMutable = { mutable First: string; Last: string }
let name = { First = "F"; Last = "L" }
name.First <- "new F"
There are a few cases where such a need may arise (performance-critical sections of code for instance). Typically, such an implementation goes against the idealogy of functional programming.
Records are not Classes
On the surface records and classes may seem similar, but there are some key differences in practice:
There are no constructors, construction is done via Record Expressions
Like classes, there are no fields, properties, getters/setters. Just fields.
When comparing two instances of a class for equality, reference is what's compared. The
Equals
method is overridden to change this behavior. With records, equality comparison is based on structural equality, that is, to compare the value of fields.
Records with Members
You can create record types with member functions.
type Employee =
{ Name: FullName
Age: int
EmployeeId: string }
// static member
static member Create(first, last, age, employeeId) =
{ Name = { First = first; Last = last }
Age = age
EmployeeId = employeeId }
// overridden Object.ToString
override this.ToString() =
$"Name: {this.Name}, Age: {this.Age}, RollNo: {this.EmployeeId}"
// Instance member
member this.WithNameInUpperCase() =
{ this with
Name =
{ First = this.Name.First.ToUpper()
Last = this.Name.Last.ToUpper() } }
Create
is a static member function that returnsEmployee
ToString
andWithNameInUpperCase
are instance member functionsToString
is to override the default ToString implementationWithNameInUpperCase
returnsEmployee
using copy and update record expression
Anonymous Records
Anonymous records allow the creation of record values without any formal/prior definition.
let someRecord = {| Name = "Whatever"; Age = 0 |}
let someNestedRecord = {| Name = "Whatever"; Age = 0; Address = {| City = "City"; State = "State"; Country = "Country" |} |}
Anonymous records, like anonymous functions, should be created for one-off cases when the formal definition is overhead.
Discriminated Unions
Discriminated unions make F# very interesting. Simply put, discriminated unions are a combination of C-style enums and unions.
Enums: select one of many options. For instance, select one from the months, days of the week, or payment methods.
Unions: different data for different cases.
Here is a simple example: type IntOrString = | I of int | S of string
IntOrString
is a discriminated union type. Values of type IntOrString
can be in two states only. These states are also known as cases.
Case
I
, when it holds anint
valueCase
S
, when it holds astring
value
Here are a few more examples:
// int value for I case
// int * int for T case
type IntOrTuple =
| I of int
| T of int * int
// Only cases, no data association
type Day =
| Monday
| Tuesday
| Wednesday
| Thursday
| Friday
| Saturday
| Sunday
// name of type string for NonEmployee case
// name of type string and employeeId of type string for Employee case
type Person =
| NonEmployee of name: string
| Employee of name: string * employeeId: string
Construction
Values of type discriminated unions are constructed by the case name, aka constructor function.
let a = I(1) // IntOrTuple.I
let b = T(1 * 2) // IntOrTuple.T
Values a
and b
are of type IntOrTuple
.
let d = Monday
let e = Sunday
Values d
and e
are of type Day
.
let f = NonEmployee("JFK")
let g = NonEmployee(name = "JFK")
let h = Employee("JFK", "12345")
let i = Employee(name = "JFK", employeeId = "12345")
Values f
, g
, h
and i
are all of type Person
.
Discriminated Unions with Members
Similar to records, discriminated unions can have member functions. Please see above.
Problems and Limitations with Basic Data Types
Basic data types such as string
and int
have two major issues:
Every
string
is just astring
, regardless of what it contains. Imagine you have a function that accepts one parameter namedemail
of typestring
. There is no direct and simple way to stop this function from being called for invalid email addresses. Anystring
can be passed to this function. Similarly, if there is a record with a field namedEmail
of typestring
, there is no stopping from record construction with anystring
value.The second issue is a source of bugs. Look at the code given below. The function
increaseHeight
expects height, anint
. However, when it is called,age
is passed, which is also anint
. There is no stopping this. This creates a bug, causing unexpected behavior.let increaseHeight h = h + 2 let age = 10 // age in years let height = 100 // height in CM let newHeight = increaseHeight age
Before we discuss aswer to these problem, we need to go through few more basic concepts, which we shall in the coming chapters.
If you have reached so far, congratulations.
Keep reading!