In the last post we talked about tuples and how they’re a convenient construct for grouping data. For all their convenience though sometimes tuples aren’t quite powerful enough. Sometimes we want to name the values with something more descriptive than fst or snd. Sometimes we may even want to attach additional members to the type. In these cases we may be inclined to fall back on traditional OOP and create a class – OOP is one of the paradigms supported by F#, after all. In some cases a class may be appropriate but F# offers a syntactically lighter-weight alternative – record types.
Although at their core record types are classes in MSIL but in F# they differ from classes in a few important ways. First, record types have structural equality rather than reference equality. This is achieved by generating overrides for CompareTo, GetHashCode, and Equals. Next, every label is automatically exposed as a read-only property. Finally, record types give us no control over the constructor.
Defining and Creating Record Types
Defining a record type is much easier than defining a class in F#. In keeping with the circle measurements theme from the tuple post, we can create a record type to contain the same measurements as the tuple as follows:
> type CircleMeasurements = { Diameter : float; Area : float; Circumference : float; };; type CircleMeasurements = {Diameter: float; Area: float; Circumference: float;}
Each of the names within the record definition (the names between the curly braces) are called labels and they must have a type annotation. If we wanted to put each label on its own line we could omit the semicolons.
Creating instances of record types is accomplished with record expressions. With basic record expressions we assign a value to each label:
> let measurements = { Diameter = 5.0; Area = 19.63495408; Circumference = 15.70796327 };; val measurements : CircleMeasurements = {Diameter = 5.0; Area = 19.63495408; Circumference = 15.70796327;}
Just as with defining the type, we can omit the semicolons between the values if we place them each on a separate line.
Conflict Resolution
Occasionally we’ll run across multiple record types with the same labels. When this happens, the type inference engine can’t resolve the proper record type and we need to provide a little more information to the compiler to ensure that the correct type is selected. Consider a system that has both favorites and bookmarks defined as record types:
> type Favorite = { Name : string; Url : string };; type Favorite = {Name: string; Url: string;} > type Bookmark = { Name : string; Url : string };; type Bookmark = {Name: string; Url: string;}
You can see that both of these record types has the labels Name and Url. If we were to create an instance as we saw above the compiler will infer that we want the Bookmark type because it was evaluated more recently than Favorite.
> let mySite = { Name = "Didactic Code"; Url = "http://davefancher.com" };; val mySite : Bookmark = {Name = "Didactic Code"; Url = "http://davefancher.com";}
If we really need to use the Favorite type instead of Bookmark we need to explicitly identify the it by prefixing it on the first label:
> let mySite = { Favorite.Name = "Didactic Code"; Url = "http://davefancher.com" };; val mySite : Favorite = {Name = "Didactic Code"; Url = "http://davefancher.com";}
Copy & Update
As with everything else in F#, record types are immutable by default. The implication for record types is that if we want to make a change to an existing instance we need to copy that instance and provide new values where appropriate. Record expressions not only allow creating new instances from scratch but also provide a convenient copy & update syntax. Consider the Favorite instance from the last section. If we needed to change the Url value we could use the copy & update syntax and specify mySite as the template:
> let myOtherSite = { mySite with Url = "http://www.davefancher.com" };; val myOtherSite : Favorite = {Name = "Didactic Code"; Url = "http://www.davefancher.com";}
With copy & update syntax the system will copy forward any values not explicitly assigned. If we were changing multiple values we’d follow the same pattern, separating each label/value pair with a semicolon.
Mutability
If we actually do want to allow changes to a record we can easily make individual labels mutable by prefixing each label we want to make mutable with the mutable keyword. For instance, we can change the type definition for the Favorite type to allow changing the Name as follows:
> type Favorite = { mutable Name : string; Url : string };; type Favorite = {mutable Name: string; Url: string;}
Creating an instance of the Favorite type is the same as we’ve already seen:
> let mySite = { Name = "My Blog"; Url = "http://davefancher.com" };; val mySite : Favorite = {Name = "My Blog"; Url = "http://davefancher.com";}
Changing the mutable value is the same as changing any other mutable value in F#:
> mySite.Name <- "Didactic Code";; val it : unit = () > mySite;; val it : Favorite = {Name = "Didactic Code"; Url = "http://davefancher.com";}
Additional Members
Like classes, record types can have additional members such as methods defined on them. If we wanted to extend our Favorites type to include a method to open the associated page we could do so with a simple Visit method that invokes Process.Start.
> open System.Diagnostics;; > type Favorite = { Name : string; Url : string } member this.Visit() = Process.Start("IExplore.exe", this.Url);; type Favorite = {Name: string; Url: string;} with member Visit : unit -> Process end
Invoking the method is just like invoking a method on any other type in F#:
> let mySite = { Name = "Didactic Code"; Url = "http://davefancher.com" };; val mySite : Favorite = {Name = "Didactic Code"; Url = "http://davefancher.com";} // Remove the pipe forward to ignore to see process details mySite.Visit() |> ignore
Pattern Matching
One area that I really haven’t devoted enough space to so far is F#’s pattern matching. I plan to explore pattern matching in much more detail in a future post so for now, if you’re not already familiar with the concept it’s enough to think of it as a much more powerful version of C#’s switch statements (or VB’s select case).
When using record types with pattern matching we need to use a record pattern. Record patterns deconstruct the record into it’s components so we can work with them individually. Record patterns don’t require us to use every value from the record either so we’re free to only include the relevant parts.
Identifying a point’s quadrant in a two-dimensional coordinate plane lets us see most of these concepts in action.
> type Point = { X : float; Y : float } let FindQuadrant point = match point with | { X = 0.0; Y = 0.0 } -> "origin" | { X = 0.0 } -> "y-axis" | { Y = 0.0 } -> "x-axis" | p when p.X > 0.0 && p.Y > 0.0 -> "I" | p when p.X < 0.0 && p.Y > 0.0 -> "II" | p when p.X < 0.0 && p.Y < 0.0 -> "III" | p when p.X > 0.0 && p.Y < 0.0 -> "IV" | _ -> failwith "unknown";; type Point = {X: float; Y: float;} val FindQuadrant : Point -> string
In this example we see matching against both the X and Y coordinate to determine if the point is the origin. We then see matching against either the X or Y coordinate to determine if the point is on an axis. With the special cases accounted for we move on to using when conditions to check for the individual quadrants. Finally, we have a wildcard pattern that uses failwith to throw an exception.
Wrapping Up
Record types provide a concise and convenient syntax for quickly creating types with named values and optional members.
In the next post we’ll explore another one of F#’s powerful constructs, the discriminated union, and see how it can help us quickly create simple object hierarchies.
2 comments
Comments are closed.