We presented before the need to aggregate useful information of some entities that cannot be restricted to have some specific types, for example, the data of a credit card.
Pretty much any high level language provides a way to define such types. In C language one has the struct
type, in Python a class without methods (or a named tuple), etc. This type in F# is called a record.
type CreditCard =
{
HoldersName : string
Number: string
ExpirationDateMonth: uint8
ExpirationDateYear: uint8
CVV: uint16
}
The record uses curly braces to aggregate the different components of the type. Each component has a label (HoldersName
, Number
, etc.) and a type associated with it.
To create a record, one needs to define all and every component:
let doeCard =
{
HoldersName = "John Doe"
Number = "1234 5678 9101 1121"
ExpirationDateMonth = 12uy
ExpirationDateYear = 23uy
CVV = 111us
}
đź”” Note the suffix
uy
for unsigned integers of 8 bits (uint8
) andus
for their 16 bits partner (uint16
).
Instead of indenting the definition, one can write all the components together, separated by ;
:
let doeFakeCard = { HoldersName = "John Doe"; Number = "1234 5678 9101 1121"; ExpirationDateMonth = 12uy; ExpirationDateYear = 23uy ; CVV = 111us}
printfn "%A" doeFakeCard
{ HoldersName = "John Doe"
Number = "1234 5678 9101 1121"
ExpirationDateMonth = 12uy
ExpirationDateYear = 23uy
CVV = 111us }
but as one can sees that this is suitable only for small records.
What happens if John Doe’s card expires and needs to be replaced by a new one?
As with all other values in the language, records are inmutable, so it is not possible to update doeCard
in place. To do that, we need to create another, new value. F# provides a way to copy and update a record value, that enables us to change just the components that need to be changed in a record. Assuming that the new card keeps the number (and, of course, the cardholder’s name), we would have:
let newDoeCard =
{ doeCard with
ExpirationDateMonth = 12uy
ExpirationDateYear = 25uy
CVV = 222us
}
printfn "%A" newDoeCard
{ HoldersName = "John Doe"
Number = "1234 5678 9101 1121"
ExpirationDateMonth = 12uy
ExpirationDateYear = 25uy
CVV = 222us }
One uses again curly braces to express the record type, then the old value doeCard
that will be updated with
the components that need to be updated.
To access a specific component of a record, one uses again the .
, as we did with discriminated unions:
printfn "John's Does card number: %A" newDoeCard.Number
printfn "John's Does card CVV: %A" newDoeCard.CVV
John's Does card number: "1234 5678 9101 1121"
John's Does card CVV: 222us
Discriminated union and record are the two ways one can represent entities in the language. One can build all sort of complex types by mixing them, it is up to the programmer how to combine this smaller bricks to model the domain.
For example, one can put together the expiration date in its own type:
type ExpirationDate =
{
Month : uint8
Year : uint8
}
That would give us a cleaner CreditCard2
type:
type CreditCard2 =
{
HoldersName : string
Number: string
ExpirationDate: ExpirationDate
CVV: uint16
}
For the vending machine, one can write
type FoodProduct =
| Chips
| Chocolate
| Candy
type BrandedFood =
| Chips of string
| Chocolate of string
| Candy of string
type FoodMachineItem =
{
Brand: BrandedFood
ProductType: FoodProduct
Price: float
}
let sourCandy = {
Brand = BrandedFood.Candy "TearDrops"
ProductType = FoodProduct.Candy
Price = 2.39
}
Notice that we need to specify completely the type in the ProductType
component, by using FoodProduct.Candy
. This is to avoid the collision with the case Candy of string
in the BrandedFood type. Do not worry! The compiler behind will get you covered, signaling the problem:
let sourCandyWithCollision = {
Brand = BrandedFood.Candy "TearDrops"
ProductType = Candy
Price = 2.39
}
input.fsx (3,19)-(3,24) typecheck error This expression was expected to have type
'FoodProduct'
but here has type
'string -> BrandedFood'
And from here on, the sky is the limit.