In addition to classes, F# also provides record types. They offer a few benefits that standard classes do not.
Getting Personal
Defining a record type is straightforward:
type Person = { First : string; Last : string }
Person
has two fields, First
and Last
, both of type string
. Defining a Person
variable is easy:
let me = { First = "Brad"; Last = "Collins" }
Pattern matching on record objects is pretty straightforward, too:
let greet = function | { First = "Brad"; Last = "Collins" } -> "It's me!" | { First = "Brad" } -> "Nice name" | { Last = "Collins" } -> "Greetings, kinfolk" | _ -> "Hello, stranger" let me = { First = "Brad"; Last = "Collins" } let kin = { First = "Shad"; Last = "Collins" } let namesake = { First = "Brad"; Last = "Rollins" } let stranger = { First = "Ezra"; Last = "Shemiah" } let meGreeting = greet me // val meGreeting : string = "It's me!" let kinGreeting = greet kin // val kinGreeting : string = "Greetings, kinfolk" let namesakeGreeting = greet namesake // val namesakeGreeting : string = "Nice name" let strangerGreeting = greet stranger // val strangerGreeting : string = "Hello, stranger"
First, notice that you do not have to tell greet
what kind of record type you want to match on. F# infers it.
Next, to match on a Person
record with specific First
and Last
fields, specify both of them, as in the first match case above. To match on just one field, either First
or Last
, specify the field to match, and use the wildcard pattern, the underscore (_
), for the field you don’t care about, as in the second and third match cases above. If Person
had more than two fields, you could match any subset of them by specifying the field values you need to be exact and using the wildcard pattern for the fields you don’t.
One other thing to note here: Order matters. What if you had defined greet
this way?
let greet = function | { First = "Brad" } -> "Nice name" | { Last = "Collins" } -> "Greetings, kinfolk" | { First = "Brad"; Last = "Collins" } -> "It's me!" // OOPS! | _ -> "Hello, stranger"
The third case (lines 5 and 6 above) would never match because { First = "Brad"; Last = "Collins" }
would always match the first case. So, pay attention out there.
Personal Improvement
As with classes, we can extend record types with member functions and properties:
type Person = { First : string Last : string } with static member Anonymous = { First = ""; Last = "" } static member Create first last = { First = first Last = last } member x.Swap () = Person.Create x.Last x.First let me = Person.Create "Brad" "Collins" // val me : Person = {First = "Brad"; // Last = "Collins";} let swapped = me.Swap() // val swapped: Person = {First = "Collins"; // Last = "Brad";}
This example also demonstrates one of the extras you get with record types: A ToString()
implementation. With a regular class, if you want ToString()
to show you something more descriptive than a type name, you have to overload System.Object.ToString()
yourself.
Record types also throw in implementations of Equals(o)
and GetHashCode()
for no charge:
let me = Person.Create "Brad" "Collins" let swapped = me.Swap() let myself = Person.Create "Brad" "Collins" let iAm = me = myself // val iAm : bool = true let iAint = me = swapped // val iAint : bool = false let meHash = me.GetHashCode() // val meHash : int = 1825427853 let myselfHash = myself.GetHashCode() // val myselfHash : int = 1825427853 let swappedHash = swapped.GetHashCode() // val swappedHash : int = 39593589
Notice how me
and myself
are equal though they are different object instances, and they also have the same hash code.
Finally, if you have a record instance and need a new instance with, say, only one or two field values that are different, F# provides a way to do that using the with
keyword so that you don’t have to set every field value explicitly:
let me = Person.Create "Brad" "Collins" let kin = { me with First = "Shad" } // val kin : Person = {First = "Shad"; // Last = "Collins";} let namesake = { me with Last = "Rollins" } // val namesake : Person = {First = "Brad"; // Last = "Rollins";}
Identity Crisis
But what happens when you have two record types that coincidentally have the same members? Maybe you have a type representing polar coordinates type and a type representing an arc. Both of them contain a radius member, R
, and an angle member, Θ
:
type Polar = { R : float; Ï´ : float } type Arc = { R : float; Ï´ : float }
What happens when you want to calculate the length of an arc, and you want to constrain arcs to “valid” angles, that is, between zero and a whole circle, nothing more than a circle or negative?
let π = System.Math.PI let length arc = match arc with | { ϴ = a } when a < 0.0 || (2.0 * π) < a -> raise <| System.ArgumentException "ϴ is out of range" | { R = radius; ϴ = angle } -> radius * angle
F# yields a warning:
The field labels and expected type of this record expression or pattern do not uniquely determine a corresponding record
In other words, F# doesn’t know whether you want to match on a Polar
object or an Arc
object. You can fix this a couple of ways. One way is to specify the type of the arc
parameter:
let π = System.Math.PI let length (arc : Arc) = match arc with | { ϴ = a } when a < 0.0 || (2.0 * π) < a -> raise <| System.ArgumentException "ϴ is out of range" | { R = radius; ϴ = angle } -> radius * angle let quarter = { R = 2.5; ϴ = (π / 2.0) } let len = length quarter // val len : float = 3.926990817
Another option is to prepend the Θ
field in the match expression with the Arc
type:
let π = System.Math.PI let length arc = match arc with | { Arc.ϴ = a } when a < 0.0 || (2.0 * π) < a -> raise <| System.ArgumentException "ϴ is out of range" | { R = radius; ϴ = angle } -> radius * angle let quarter = { R = 2.5; ϴ = (π / 2.0) } let len = length quarter // val len : float = 3.926990817
Similarly, to remove any ambiguity when creating a record instance, prepend the type name to the beginning of one of the field names:
let π = System.Math.PI let arc = { Arc.R = 2.5; ϴ = (π / 2.0) } let polar = { Polar.R = 1.75; ϴ = (1.25 * π) }
One reply on “F# Friday – Pattern Matching, Part 2: Record Types”
[…] F# Friday – Pattern Matching, Part 2: Record Types – Brad Collins […]