Categories
Tech

F# Friday – Pattern Matching, Part 2: Record Types

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”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.