© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. EasonStylish F# 6https://doi.org/10.1007/978-1-4842-7205-3_7

7. Record Types

Kit Eason1  
(1)
Farnham, Surrey, UK
 

Proper storage is about creating a home for something so that minimal effort is required to find it and put it away.

—Geralin Thomas, Organizing Consultant

Winning with Records

Record types are a simple way of recording small groups of values. You define a set of names and corresponding types; then you can create, compare, and amend instances of these groupings with some extremely simple syntax. But behind this simplicity lies some powerful and well-thought-out functionality. Learn to wield record types effectively and you’ll be well on the way to becoming an expert F# developer. It’s also worth knowing when not to use record types and what the alternatives are in these circumstances. We’ll cover both explicitly declared named record types and also implicitly declared anonymous record types.

Record Type Basics

Declaring and instantiating a record type could hardly be easier. You define the names (field labels) of the items you want the record to contain, together with their types, all in curly braces (Listing 7-1).
        open System
        type FileDescription = {
            Path : string
            Name : string
            LastModified : DateTime }
Listing 7-1

Declaring a record type

Then you create instances simply by binding values to each name, again in curly braces (Listing 7-2).
        open System.IO
        let fileSystemInfo (rootPath : string) =
            Directory.EnumerateFiles(rootPath, "*.*",
                                     SearchOption.AllDirectories)
            |> Seq.map (fun path ->
                { Path = path |> Path.GetDirectoryName
                  Name = path |> Path.GetFileName
                  LastModified = FileInfo(path).LastWriteTime })
Listing 7-2

Instantiating record type instances

Note that at instantiation time, you don’t have to mention the name of the record type itself, just its fields. The exception to this is when two record types have field names in common, in which case you may have to prefix the first field name in the binding with the name record type you want, for example, { FileDescription.Path = ... .

You can access the fields of record type instances using dot-name notation, exactly as if they were C# class members (Listing 7-3).
    // Name: ad.png Path: c: emp Last modified: 15/08/2017 22:07:34
    // Name: capture-1.avi Path: c: emp Last modified: 27/02/2017 22:04:31
    // ...
    fileSystemInfo @"c: emp"
    |> Seq.iter (fun info -> // info is a FileDescription instance
        printfn "Name: %s Path: %s Last modified: %A"
            info.Name info.Path info.LastModified)
Listing 7-3

Accessing record type fields using dot notation

Record Types and Immutability

Like most things in F#, record types are immutable by default. You can in principle bind the whole record instance as mutable using let mutable (Listing 7-4), but this means that the entire record instance can be replaced with a new and different record using the <- operator. It does not make the individual fields mutable. In practice, I can’t remember ever declaring an entire record to be mutable.
        type MyRecord = {
            String : string
            Int : int }
        let mutable myRecord =
            { String = "Hullo clouds"
              Int = 99 }
        // {String = "Hullo clouds";
        //  Int = 99;}
        printfn "%A" myRecord
        myRecord <-
            { String = "Hullo sky"
              Int = 100 }
        // {String = "Hullo sky";
        //  Int = 100;}
        printfn "%A" myRecord
Listing 7-4

Declaring a record instance as mutable

What about making the fields of the record mutable? This is certainly possible (Listing 7-5), and having done this, you can assign into fields using <-. This isn’t quite as unheard of as declaring whole records mutable, but it’s still rare. I guess there might be performance-related cases where this might be desirable, but again I can’t recall doing it myself.
        type MyRecord = {
            mutable String : string
            mutable Int : int }
        let myRecord =
            { String = "Hullo clouds"
              Int = 99 }
        // {String = "Hullo clouds";
        //  Int = 99;}
        printfn "%A" myRecord
        myRecord.String <- "Hullo sky"
        // { String = "Hullo sky";
        //   Int = 99;}
        printfn "%A" myRecord
Listing 7-5

Declaring record fields as mutable

By far, the most common and idiomatic way of “amending” record types is using the not-very-snappily-named copy-and-update record expression (Listing 7-6).
        type MyRecord = {
            String : string
            Int : int }
        let myRecord =
            { String = "Hullo clouds"
              Int = 99 }
        // {String = "Hullo clouds";
        //  Int = 99;}
        printfn "%A" myRecord
        let myRecord2 =
            { myRecord with String = "Hullo sky" }
        // { String = "Hullo sky";
        //   Int = 99;}
        printfn "%A" myRecord2
Listing 7-6

“Amending” a record using copy and update

In a copy-and-update operation, all the fields of the new record are given the values from the original record, except those given new values in the with clause. Needless to say, the original record is unaffected. This is the idiomatic way to handle “changes” to record type instances.

Default Constructors, Setters, and Getters

One downside to immutability by default: you may occasionally have problems with external code (particularly serialization and database code) failing to instantiate record types correctly, or throwing compilation errors about default constructors. In these cases, simply add the [<CLIMutable>] attribute to the record declaration. This causes the record to be compiled with a default constructor and getters and setters, which the external framework should find easier to cope with.

Records vs. Classes

Records offer a nice, concise syntax for grouping values, but surely they aren’t that different from the conventional “object” of object orientation (which are known in F# as class types or just classes). After all, if we make a class-based version of Listings 7-1 and 7-2, the code doesn’t look all that different and seems to behave exactly the same (Listing 7-7).
    open System
    type FileDescriptionOO(path:string, name:string, lastModified:DateTime) =
        member __.Path = path
        member __.Name = name
        member __.LastModified = lastModified
    open System.IO
    let fileSystemInfoOO (rootPath : string) =
        Directory.EnumerateFiles(rootPath, "*.*",
                                    SearchOption.AllDirectories)
        |> Seq.map (fun path ->
            FileDescriptionOO(path |> Path.GetDirectoryName,
                              path |> Path.GetFileName,
                              (FileInfo(path)).LastWriteTime))
Listing 7-7

F# Object-Oriented class types vs. records

We’ll look properly at classes in Chapter 8, but it’s fairly easy to see what is going on here. The class we make is even immutable. So do we really need to bother with record types? In the next few sections, I’ll discuss some of the advantages (and a few disadvantages!) of using record types.

Structural Equality by Default

Consider the following attempt to represent a position on the Earth’s surface with a class, using latitude and longitude (Listing 7-8).
        type LatLon(latitude : float, longitude : float) =
            member __.Latitude = latitude
            member __.Longitude = longitude
Listing 7-8

Representing latitude and longitude using a class

You might think that if two positions have the same latitude and longitude values, they would be considered equal. But with a class, they are not1 (Listing 7-9).
        let waterloo = LatLon(51.5031, -0.1132)
        let victoria = LatLon(51.4952, -0.1441)
        let waterloo2 = LatLon(51.5031, -0.1132)
        // false
        printfn "%A" (waterloo = victoria)
        // true
        printfn "%A" (waterloo = waterloo)
        // false!
        printfn "%A" (waterloo = waterloo2)
Listing 7-9

Some types are less equal than others

This is because classes in both F# and C# have what is called reference or referential equality by default, which means that to be considered equal, two values need to represent the same physical object in memory. Sometimes, as in the LatLon example, this is very much not what you want.

The conventional way around this in C# (and you can do the same for classes in F#) is to write custom code that decides whether two instances are equal in some meaningful sense. The trouble is in practice this is quite an endeavor, requiring you to override Object.Equals, implement System.IEquatable, override Object.GetHashCode, and (admittedly optionally) override the equality and inequality operators. Who has time for all that? (I will show how to do it in Chapter 8, just in case you do have time!)

Record types, by contrast, have what is called structural equality. (I think that’s a terrible name, so I always mentally translate this to content equality.) With structural equality, two items are considered equal if all their fields are equal. Listing 7-10 shows the LatLon issue being solved simply by using a record instead of a class.
        type LatLon = {
            Latitude : float
            Longitude : float }
        let waterloo = { Latitude = 51.5031; Longitude = -0.1132 }
        let victoria = { Latitude = 51.4952; Longitude = -0.1441 }
        let waterloo2 = { Latitude = 51.5031; Longitude = -0.1132 }
        // false
        printfn "%A" (waterloo = victoria)
        // true
        printfn "%A" (waterloo = waterloo)
        // true
        printfn "%A" (waterloo = waterloo2)
Listing 7-10

Default structural (content) equality with record types

You can mess things up again, though, if one of the fields of your record is itself of a type that implements referential equality. This is because, under those circumstances, the records’ fields aren’t all equal by their own types’ definitions of “equal” so the records won’t be considered equal. Listing 7-11 shows an example of this happening.
        type Surveyor(name : string) =
            member __.Name = name
        type LatLon = {
            Latitude : float
            Longitude : float
            SurveyedBy : Surveyor }
        let waterloo =
            { Latitude = 51.5031
              Longitude = -0.1132
              SurveyedBy = Surveyor("Kit") }
        let waterloo2 =
            { Latitude = 51.5031
              Longitude = -0.1132
              SurveyedBy = Surveyor("Kit") }
        // true
        printfn "%A" (waterloo = waterloo)
        // false
        printfn "%A" (waterloo = waterloo2)
Listing 7-11

Do all the fields of your record implement the right equality?

Because they use different instances of the Surveyor class, the instances waterloo and waterloo2 aren’t considered equal, even though from a content point of view, the surveyors have the same name. If we had created one Surveyor instance in advance and used that same instance when creating each of the LatLon instances, waterloo and waterloo2 would have been equal again! The general solution to this would be either to use a record for the Surveyor type or override the Surveyor equality-checking logic. Although worth bearing in mind, this issue rarely comes up in practice.

Another edge case is when you actually want records to have referential equality. That’s easy: add the [<ReferenceEquality>] attribute (Listing 7-12).
        [<ReferenceEquality>]
        type LatLon = {
            Latitude : float
            Longitude : float }
        let waterloo = { Latitude = 51.5031; Longitude = -0.1132 }
        let waterloo2 = { Latitude = 51.5031; Longitude = -0.1132 }
        // true
        printfn "%A" (waterloo = waterloo)
        // false
        printfn "%A" (waterloo = waterloo2)
Listing 7-12

Forcing reference equality for record types

Once again, I can’t ever recall having to use the ReferenceEquality attribute in real code. If you do use it, remember you won’t be able to sort instances using default sorting because the attribute disables greater than/less than comparison. While we are on the subject, you can also add the NoEquality attribute to disable “equals” and “greater/less than” operations on a record type, or you can even disable “greater/less than” operations while allowing “equals” operations using the NoComparison attribute . I have seen the NoEquality attribute used precisely once in real code. Stylistically, I would say that – given what records are for – use of ReferenceEquality, NoEquality, and NoComparison attributes in general “line of business” code is probably a code smell, though they no doubt have their place in highly technical realms.

Be aware that the ReferenceEquality, NoEquality, and NoComparison attributes are all F# specific. Other languages are under no obligation to respect them (and probably won’t).

Records as Structs

Another possible reason to favor records is that, subject to certain restrictions, they can easily be marked as structs. This affects the way they are stored. To quote the official documentation:

Structures are value types, which means that they are stored directly on the stack or, when they are used as fields or array elements, inline in the parent type.

You make a record of a struct simply by adding the [<Struct>] attribute. As Listing 7-13 shows, this can have a substantial effect on performance.
    type LatLon = {
        Latitude : float
        Longitude : float }
    [<Struct>]
    type LatLonStruct = {
        Latitude : float
        Longitude : float }
    let sw = System.Diagnostics.Stopwatch.StartNew()
    let llMany =
        Array.init 1_000_000 (fun x ->
            { LatLon.Latitude = float x
              LatLon.Longitude = float x } )
    // Non struct: 51ms
    printfn "Non struct: %ims" sw.ElapsedMilliseconds
    sw.Restart()
    let llsMany =
        Array.init 1_000_000 (fun x ->
            { LatLonStruct.Latitude = float x
              LatLonStruct.Longitude = float x } )
    // Struct: 17ms
    printfn "Struct: %ims" sw.ElapsedMilliseconds
Listing 7-13

Marking a record type as a struct

Scenarios vary widely in regard to creating, accessing, copying, and releasing instances, so you should experiment diligently in your use case, rather than blindly assuming that using the Struct attribute will solve any performance woes.

There is one significant implication of using struct records: if you want any field of the record type to be mutable, you must declare the whole instance as mutable too, as in Listing 7-14.
        [<Struct>]
        type LatLonStruct = {
            mutable Latitude : float
            mutable Longitude : float }
        let waterloo = { Latitude = 51.5031; Longitude = -0.1132 }
        // Error: a value must be mutable in order to mutate the contents.
        waterloo.Latitude <- 51.5032
        let mutable waterloo2 = { Latitude = 51.5031; Longitude = -0.1132 }
        waterloo2.Latitude <- 51.5032
Listing 7-14

Struct records must be mutable instances to mutate fields

Mapping from Instantiation Values to Members

The final, and for me, clinching advantage of records over classes is the direct and complete mapping from what you provide when creating instances to what you get back when consuming instances. If you create a LatLon record instance by providing a latitude and longitude, then you automatically know the following facts when you later consume the instance:
  • You can get all the values back that you originally provided and in their original form.

  • You can’t get anything else back other than what you provided (unless you define members on the record type, which is possible but rare).

  • You can’t create an instance without providing all the necessary values.

  • Nothing can change the values you originally provided – unless you declare fields as mutable, which generally is unwise.

These may seem like small points, but they contribute greatly to the motivational transparency and semantic focus of your code. As an example, consider the third point: You can’t create an instance without providing all the necessary values. Contrast that with the coding pattern that any experienced OO developer has seen, where you need to both construct an object instance and set some properties in order for the object to become usable. (Any place you use object-initializer syntax to get to a usable state is an example.) The fact that, in order to create a record, you have to provide values for all its fields has an interesting consequence: if you add a field, you’ll have to make code changes everywhere that record is instantiated. This is true even if you make the field an option type – there is no concept in record instantiation of default values for fields, even ones that are option types. At first, this can seem annoying, but it is actually a very good thing. All sorts of subtle bugs can creep in if it’s possible to add a property to a type without making an explicit decision about what that property should contain, everywhere the type is used. Those compiler errors are telling you something!

Records Everywhere?

If the case for record types is so compelling, why don’t we use them everywhere? Why does F# even bother to offer OO-style class types? Are these just a concession to C# programmers, to be avoided by the cool kids?

The answer is “no”; class types definitely have a place in F# code. I’ll go into detail on class types in Chapter 8, but just to balance all the positive things I’ve said about record types, Table 7-1 shows some reasons why you might not want to use them, together with some suggestions for alternatives.
Table 7-1

When to Consider Not Using Record Types

Scenario

Consider instead

External and internal representations of data need to differ

Class types

Need to participate in an inheritance hierarchy – either to inherit from or be inherited from in a traditional OO sense

Class types

Need to represent a standard set of functions, with several realizations that share function names and signatures, but have different implementations

F# interfaces and/or abstract types, inherited from by class types

The last of these points bears a little elaboration. From time to time, I have come across code bases where records of functions have been used as a supposedly more functional alternative to interfaces. In principle, this does have a few advantages:
  • Unlike code that uses interfaces, you don’t have to upcast to the interface type whenever you want to use the interface. (I give a few more details of this in Chapter 8.)

  • It can make it easier to use partial application when using the “pretend interface.”

  • It’s sometimes claimed to be more concise.

The MSDN F# Style Guide comes out firmly against records-as-interfaces, and, having worked with a substantial code base where records were used in this way, so do I! To quote the guide:

Use interface types to represent a set of operations. This is preferred to other options, such as tuples of functions or records of functions… Interfaces are first-class concepts in .NET....

In my experience, use of records-as-interfaces leads to unfriendly, incomprehensible code. When editing, one rapidly gets into the situation where everything has to compile before anything will compile. In concrete terms, your screen fills with red squiggly lines, and it’s very hard to work out what to do about it! With true interfaces, by contrast, the errors resulting from incomplete or slightly incorrect code are more contained, and it’s much easier to work out if an error results, for example, from a wrongly implemented method or from a completely missing one. Interfaces play more nicely with Intellisense as well. As for the supposed advantage of partial application – well, I’d much rather maintainers (including my future self) have some idea of what is going on than save a few characters by not repeating a couple of function parameters.

I’m not saying, by the way, that records shouldn’t implement interfaces, which they can do in exactly the same way as I show with classes in Chapter 8. If you find that useful, it’s fine.

One notable exception to what I’ve said previously is when working with “Fable Remoting.” Fable, in case you haven’t come across it, is an F#-to-JavaScript compiler that allows you to write both the back and front end of a web application in F#. This architecture requires the ability to make function calls from the front end, in the browser, to the back end - a .NET program running on the server. Without going into detail here, it turns out that describing such an interface in terms of a record-of-functions works very well in that specialized case.

Pushing Records to the Limit

Now that you’re familiar with how and when to use basic record types, it’s time to look at some of the more exotic features and usages that are available. Don’t take this section as encouragement to use all the techniques it describes. Some (not all) of these tricks really are rarities, and when it’s truly necessary to use them, you’ll know.

Generic Records

Records can be generic – that is, you can specify the type (or types) of the fields, as a kind of meta-property of the record type. The meta-property is called a type parameter. Listing 7-15 shows a LatLon record that could use any type for its Latitude and Longitude fields .
        type LatLon<'T> = {
            mutable Latitude : 'T
            mutable Longitude : 'T }
        // LatLon<float>
        let waterloo = { Latitude = 51.5031; Longitude = -0.1132 }
        // LatLon<float32>
        let waterloo2 = { Latitude = 51.5031f; Longitude = -0.1132f }
        // Error: Type Mismatch...
        printfn "%A" (waterloo = waterloo2)
Listing 7-15

A generic record type

Note that we don’t have to specify the type to use at construction time. The simple fact that we say { Latitude = 51.5031f... versus { Latitude = 51.5031... (note the “f,” which specifies a single-precision constant) is enough for the compiler to create a record that has single-precision instead of double-precision fields. Also notice that, since waterloo and waterloo2 are different types, we can’t directly compare them using the equals operator.

What if you don’t want to leave type inference to work out the type of the generic parameter? (Very occasionally type inference can even find it impossible to work this out.) Clearly, in this case, we can’t use the trick of prefixing the first field binding with the record type name to disambiguate, as the name will be the same in each case. Instead – as in any let binding – you can specify the type of the bound value, in this case, LatLon<float> or LatLon<float32> (Listing 7-16).
        type LatLon<'T> = {
            mutable Latitude : 'T
            mutable Longitude : 'T }
        // LatLon<float>
        let waterloo : LatLon<float> = {
            Latitude = 51.5031
            Longitude = -0.1132 }
        // Error: The expression was expected to have type 'float32'
        // but here has type 'float'.
        let waterloo2 : LatLon<float32> = {
            Latitude = 51.5031f
            Longitude = -0.1132 }
Listing 7-16

Pinning down the generic parameter type of a record type

In this case, as shown in the final lines of Listing 7-16, it’s an error to try and bind a field using a value of a different type (note the missing “f” in the Longitude binding).

Recursive Records

Record types can also be recursive – that is, the type can have a field containing a value of its own type. Not easy to put into words, so jump straight to Listing 7-17, where we define a type to represent some imaginary user interface.
        type Point = { X : float32; Y : float32 }
        type UiControl = {
            Name : string
            Position : Point
            Parent : UiControl option }
        let form = {
            Name = "MyForm"
            Position = { X = 0.f; Y = 0.f }
            Parent = None }
        let button = {
            Name = "MyButton"
            Position = { X = 10.f; Y = 20.f }
            Parent = Some form }
Listing 7-17

A recursive record type

Each UiControl instance can have a parent that is itself a UiControl instance. It’s important that the recursive field (in this case, Parent ) is an option type. Otherwise, we are implying either that the hierarchy goes upward infinitely (making it impossible to instantiate) or that it is circular.

Oddly enough, it is possible to instantiate circular hierarchies, using let rec and and (Listing 7-18). I present this mainly as a curiosity – if you need to do it in practice, either you are doing something very specialized or something has gone terribly wrong in your domain modeling!
        // You probably don't want to do this!
        type Point = { X : float32; Y : float32 }
        type UiControl = {
            Name : string
            Position : Point
            Parent : UiControl }
        let rec form = {
            Name = "MyForm"
            Position =  { X = 0.f; Y = 0.f }
            Parent = button }
        and button = {
            Name = "MyButton"
            Position =  { X = 10.f; Y = 20.f }
            Parent = form }
Listing 7-18

Instantiating a circular set of recursive records

Records with Methods

Anyone with an Object-Oriented programming background will be wondering whether it’s possible for records to have methods. And the answer is yes, but it may not always be a great idea.

Instance Methods

Listing 7-19 shows us adding a Distance instance method to our familiar LatLon record and then calling it exactly as one would a class method.
type LatLon =
    { Latitude : float
      Longitude : float }
    // Naive, straight-line distance
    member this.DistanceFrom(other : LatLon) =
        let milesPerDegree = 69.
        ((other.Latitude - this.Latitude) ** 2.)
        +
        ((other.Longitude - this.Longitude) ** 2.)
        |> sqrt
        |> (*) milesPerDegree
let coleman = {
    Latitude = 31.82
    Longitude = -99.42 }
let abilene = {
    Latitude = 32.45
    Longitude = -99.75 }
// Are we going to Abilene? Because it's 49 miles!
printfn "Are we going to Abilene? Because it's %0.0f miles!"
    (abilene.DistanceFrom(coleman))
Listing 7-19

Adding an instance method to a record type

Note that the distance calculation I do here is extremely naive. In reality, you’d want to use the haversine formula, but that’s rather too much code for a book listing.

Instance methods like this work fine with record types and are quite a nice solution where you want structural (content) equality for instances and also to have instance methods to give you fluent syntax like abilene.DistanceFrom(coleman).

Static Methods

You can also add static methods. If you do this, it’s probably because you want to construct a record instance using something other than standard record construction syntax. For example, Listing 7-20 adds a TryFromString method to LatLon, which tries to parse a comma-separated string into two elements and then tries to parse these as floating-point numbers, before finally constructing a record instance in the usual curly-bracket way.
open System
type LatLon =
    { Latitude : float
      Longitude : float }
    static member TryFromString(s : string) =
        match s.Split([|','|]) with
        | [|lats; lons|] ->
            match (Double.TryParse(lats),
                   Double.TryParse(lons)) with
            | (true, lat), (true, lon) ->
                { Latitude = lat
                  Longitude = lon } |> Some
            | _ -> None
        | _ -> None
// Some {Latitude = 50.514444;
//       Longitude = -2.457222;}
let somewhere = LatLon.TryFromString "50.514444, -2.457222"
// None
let nowhere = LatLon.TryFromString "hullo trees"
printfn "%A, %A" somewhere nowhere
Listing 7-20

Adding a static method to a record type

This is quite a nice way of effectively adding constructors to record types. It might be especially useful it you want to perform validation during construction.

Method Overrides

Sometimes, you want to change one of the (very few) methods that a record type has by default. The most common one to override is ToString(), which you can use to produce a nice printable representation of the record (Listing 7-21).
type LatLon =
    { Latitude : float
      Longitude : float }
    override this.ToString() =
        sprintf "%f, %f" this.Latitude this.Longitude
// 51.972300, 1.149700
{ Latitude = 51.9723
  Longitude = 1.1497 }
|> printfn "%O"
Listing 7-21

Overriding a method on a record

In Listing 7-21, I’ve used the “%O” format specifier, which causes the input’s ToString() method to be called.

Records with Methods – A Good Idea?

I don’t think there is anything inherently wrong with adding methods to record types. You should just beware of crossing the line into territory where it would be better to use a class type. If you are using record methods to cover up the fact that the internal and external representations of some data do in fact need to be different, you’ve probably crossed the line!

There is an alternative way of associating behavior (functions or methods) with types (sets of data): group them in an F# module, usually with the same name as the type and placed just after the type’s definition. We looked at this back in Chapter 2, for example, in Listing 2-9, where we defined a MilesYards type, for representing British railroad distances, and a MilesYards module containing functions to work with the type. In my opinion, the modules approach is generally better than gluing the functions to the record in the form of methods.

Anonymous Records

Although the declaration syntax of F# records is about as lightweight as it could be, there are sometimes situations where even that overhead seems too much. For times like this, F# offers anonymous records. Anonymous records bring most of the benefits of “named” records in terms of strong typing, type inference, structural (content) equality, and so forth without the overhead of having to declare them explicitly.

Let’s say you want to work with GPS coordinates. You might use a LatLon type like the one we declared back in Listing 7-10, but with anonymous records, you can get rid of the declaration (Listing 7-22). The only tax you have to pay is to insert vertical bar (“|”) characters inside the curly brackets. Notice how you still get a type that has Latitude and Longitude fields, just like a named record.
        // {| Latitude : float; Longitude : float |}
        let waterloo = {| Latitude = 51.5031; Longitude = -0.1132 |}
        // {| Latitude : float; Longitude : float |}
        let victoria = {| Latitude = 51.4952; Longitude = -0.1441 |}
        printfn "%0.2f, %0.2f; %0.2f, %0.2f"
            waterloo.Latitude waterloo.Longitude
            victoria.Latitude victoria.Longitude
Listing 7-22

Creating anonymous records

Use of anonymous records doesn’t undermine the type safety that is the mainstay of F# code. For example, in Listing 7-23, we try to add the value 1.0f to the latitude of an anonymous latitude/longitude record, but I get an error because the type of the latitude field is a double-precision floating-point number, whereas 1.0f is single precision.
        let waterloo = {| Latitude = 51.5031; Longitude = -0.1132 |}
        // The type 'float32' does not match the type 'float'
        let newLatitude = waterloo.Latitude + 0.1f
Listing 7-23

Type safety and anonymous records

A good use of anonymous records is in pipelines where more than one value needs to be passed between successive stages. If you are using tuples in these cases, it’s very easy to mix values up or lose track of what is going on. In these cases, at least consider doing something like Listing 7-24. This code is designed to take a collection of artist names and sort them while ignoring definite and indefinite articles such as “The” in “The Bangles” and “A” in “A Flock of Seagulls.” A couple of other requirements complicate matters: the sort should be case insensitive, and a “display name” should be generated which shows the sorting value with the case preserved (e.g., “Bangles, The”).
        let artists =
            [|
                "The Bangles"; "Bananarama"; "Theo Travis"
                "The The"; "A Flock of Seagulls"; "REM"; "ABBA";
                "eden ahbez"; "Fairport Convention"; "Elbow"
            |]
        let getSortName (prefixes : seq<string>) (name : string) =
            prefixes
            |> Seq.tryFind name.StartsWith
            |> Option.map (fun prefix ->
                let mainName = name.Substring(prefix.Length)
                sprintf "%s, %s" mainName prefix)
            |> Option.defaultValue name
        let sortedArtists =
            artists
            |> Array.map (fun artist ->
                let displayName =
                    artist |> getSortName ["The "; "A "; "An "]
                {| Name = artist
                   DisplayName = displayName
                   SortName = displayName.ToUpperInvariant() |})
            |> Array.sortBy (fun sortableArtist ->
                sortableArtist.SortName)
Listing 7-24

Using anonymous records to clarify intermediate values

This could certainly be achieved by having the lambda in the Array.map operation return a tuple. But using an anonymous record makes very clear the roles that the three created values play: the original name, the display name with the article moved to the end, and the uppercased sort name. When we come to sort the results in the last line, it’s very clear that we are using SortName to sort on. Anything which consumed these results could also use these fields appropriately and unambiguously.

Another advantage of code like this is that when you use a debugger to pause program execution and view values, it’s much clearer which value is which. The field names in anonymous records are shown in the debugger.

Anonymous and Named Record Terminology

At this point, I need to make a brief point about terminology. The documentation for anonymous records contains the magnificent heading “Anonymous Records are Nominal,” which appears at first sight to be a contradiction in terms. What this is saying is that anonymous records have a “secret” name and so technically are “nominal,” even though we don’t give them a name in our code. For simplicity, in this section, I’m using named records to mean those declared up front with the type Name = { <field declarations> } syntax and instantiated later and anonymous record to mean those instantiated without prior declaration using the {| <field values> |} syntax.

Anonymous records seem to be almost too good to be true. Surely, there are some showstopping limitations. In practice, there are very few and indeed some things which you might expect would not work are in fact fine. Here are some points which might be worrying you about anonymous records.

Anonymous Records and Comparison

Anonymous records have the same equality and comparison rules as named records. Consider Listing 7-25, where I create latitude/longitude anonymous records for several locations. These can be compared using structural (content) equality: two instances that have the same coordinates are considered equal. And when the instances are compared (e.g., for sorting purposes), a sensible comparison is done using the contents of their fields.
        let waterloo = {| Latitude = 51.5031; Longitude = -0.1132 |}
        let victoria = {| Latitude = 51.4952; Longitude = -0.1441 |}
        let waterloo2 = {| Latitude = 51.5031; Longitude = -0.1132 |}
        // false
        printfn "%A" (waterloo = victoria)
        // true
        printfn "%A" (waterloo = waterloo)
        // true
        printfn "%A" (waterloo = waterloo2)
        // true, because (51.5031,-0.1132 is 'greater' than (51.4952, -0.1441)
        printfn "%A" (waterloo > victoria)
Listing 7-25

Equality and comparison of anonymous record instances

The same stipulation applies here as it does to named records: that each of the fields of the record itself has structural equality.

What happens if I instantiate anonymous records having the same fields in different parts of my code? Are these instances also type compatible? In Listing 7-26, I instantiate anonymous records in two different functions; then I have some code to return true if the types coming from those separate locations are the same. They are! From this also flows the fact that they can be compared or checked for equality just as if the two sources had returned instances of a single named type. For completeness, I also create a third anonymous record with an apparently minor difference; the fields are single-precision numbers as denoted by the f in the literals. This is a different type and so cannot be compared or checked for equality with the others.
        let getSomePositions() =
            [|
                {| Latitude = 51.5031; Longitude = -0.1132 |}
                {| Latitude = 51.4952; Longitude = -0.1441 |}
            |]
        let getSomeMorePositions() =
            [|
                {| Latitude = 51.508; Longitude = -0.125 |}
                {| Latitude = 51.5173; Longitude = -0.1774 |}
            |]
        let getSinglePositions() =
            [|
                {| Latitude = 51.508f; Longitude = -0.125f |}
                {| Latitude = 51.5173f; Longitude = -0.1774f |}
            |]
        let p1 = getSomePositions() |> Array.head
        let p2 = getSomeMorePositions() |> Array.head
        let p3 = getSinglePositions() |> Array.head
        // f__AnonymousType3108251393`2[System.Double,System.Double]
        printfn "%A" (p1.GetType())
        // f__AnonymousType3108251393`2[System.Double,System.Double]
        printfn "%A" (p2.GetType())
        // true
        printfn "%A" (p1.GetType() = p2.GetType())
        // false
        printfn "%A" (p1 = p2)
        // f__AnonymousType3108251393`2[System.Single,System.Single]
        printfn "%A" (p3.GetType())
        // false
        printfn "%A" (p1.GetType() = p3.GetType())
        // Error: Type mismatch
        printfn "%A" (p1 = p3)
Listing 7-26

Anonymous records with the same names and types of fields are the same type

“Copy and Update” on Anonymous Records

Again, the behavior is at least as good as for named records, but with a little bonus. Listing 7-27 shows us moving the position of a latitude/longitude anonymous record by 1 degree of latitude using the with keyword. You can go beyond this to create a new type, with additional fields, on the fly. Also in Listing 7-27, we create a new instance with the same latitude and longitude, but with an altitude value as well. Finally, we do both, using with to create a new instance from an existing one, having both an altered value for an existing field, and a new field. Stylistically, I think you could easily take this too far, but you may find it useful in some circumstances.
        let waterloo = {| Latitude = 51.5031; Longitude = -0.1132 |}
        let nearWaterloo =
             {| waterloo
                with Latitude = waterloo.Latitude + 1.0 |}
        let waterloo3d =
            {| waterloo
                with AltitudeMetres = 15.0 |}
        let nearWaterloo3d =
            {| waterloo
                with
                    Latitude = waterloo.Latitude + 1.0
                    AltitudeMetres = 15.0 |}
Listing 7-27

Copy-and-update operations on anonymous records

It’s also worth noting that the basis for a with construct that adds one or more fields doesn’t have to be an anonymous record, so long as it returns an anonymous record. Listing 7-28 is like Listing 7-27 except that we are “extending” a named record, the result being an anonymous record.
    type LatLon = { Latitude : float; Longitude : float }
    let waterloo = { Latitude = 51.5031; Longitude = -0.1132 }
    // {| Latitude = 52.531; Longitude = -0.1132; AltitudeMetres = 15.0 |}
    let nearWaterloo3d =
        {| waterloo
            with
                Latitude = waterloo.Latitude + 1.0
                AltitudeMetres = 15.0 |}
Listing 7-28

Creating a new anonymous record with an additional field, based on a named record

Serialization and Deserialization of Anonymous Records

Listing 7-29 shows us happily serializing and deserializing yet another anonymous latitude/longitude instance using System.Text.Json. The deserialization syntax is interesting: we don’t have a named record type to provide as the type parameter of the Deserialize method . But that’s fine in the angle brackets, we can specify the field names and types in {| |} brackets, just as they appear in the type signature of the waterloo value.
        open System.Text.Json
        let waterloo = {| Latitude = 51.5031; Longitude = -0.1132 |}
        let json = JsonSerializer.Serialize(waterloo)
        // {"Latitude":51.5031,"Longitude":-0.1132}
        printfn "%s" json
        let waterloo2 =
            JsonSerializer.Deserialize<
                {| Latitude : float; Longitude : float |}>(json)
        // { Latitude = 51.5031
        //   Longitude = -0.1132 }
        printfn "%A" waterloo2
Listing 7-29

Serializing and deserializing anonymous records

This raises an interesting possibility. If you are facing the task of deserializing some JSON from an external source, you can use exactly this syntax to keep things super lightweight. In Listing 7-30, we get a response from the PLOS Open Access science publisher, in this case, a list of papers about DNA. We are only interested in a subset of the many fields and subfields of the JSON response. We specify these as a nested, anonymous type in the type parameter of Deserialize.
open System.Net.Http
open System.Text.Json
let client = new HttpClient()
let response =
    client.GetStringAsync("http://api.plos.org/search?q=title:DNA").Result
let abstracts =
    JsonSerializer.Deserialize<
        {| response :
            {| docs :
                {| id : string; ``abstract`` : string[] |}[]
            |}
        |}>(response)
// { response =
//    { docs =
//       [|{ abstract =
//            [|"Nucleic acids, due to their structural and chemical properties, can form double-stranded secondary...
//           id = "10.1371/journal.pone.0000290" }; ...
printfn "%A" abstracts
Listing 7-30

Using anonymous records to deserialize JSON API results

There are several interesting things to note here. These points are not specific to anonymous records but do help us to keep things lightweight in this context:
  • If we are not interested in a property of the JSON, that’s fine we just don’t mention it in the anonymous record we specify in the type parameter.

  • If we happen to specify fields in the anonymous record that aren’t in the JSON, we will get nulls or zeros. (If you edit one of the field names and rerun the code, you’ll see what I mean.) You will have to watch out for mistakes like this at runtime because there is no way for the compiler to spot them.

  • If there happens to be a clash between a property name in the JSON and a reserved word in F#, you will need to put the field name in double back quotes. We have done this with the word abstract in Listing 7-30.

Anonymous Records in Type Hints

You can use anonymous records in type hints. Listing 7-31 shows a function that uses anonymous records in both the parameter part and the result part of its declaration. I’m not sure that using anonymous records in this way makes for particularly readable code, but you may find it a useful trick in specialized situations.
        let toSinglePrecision
            (latLon : {| Latitude : float; Longitude : float |})
                : {| Latitude : single; Longitude : single |} =
                    {| Latitude = latLon.Latitude |> single
                       Longitude = latLon.Longitude |> single |}
        let waterloo = {| Latitude = 51.5031; Longitude = -0.1132 |}
        let waterlooSingle = waterloo |> toSinglePrecision
Listing 7-31

Anonymous records in type hints

Struct Anonymous Records

You might remember that named records can be forced to be structs using the [<Struct>] attribute . This causes them to be stored directly on the stack or inline in their parent type or array. The syntax is a bit different for anonymous records because there is no type declaration on which to put an attribute. Instead, you use the struct keyword where you instantiate the record, just before the opening {| brackets. (Listing 7-32). You can also do this when specifying an anonymous record as the parameter of a function. Calls to that function will be inferred to be using structs as well.
        let waterloo = struct {| Latitude = 51.5031; Longitude = -0.1132 |}
        let formatLatLon
            (latLon : struct {| Latitude : float; Longitude : float |} ) =
                sprintf "Latitude: %0.3f, Longitude: %0.3f"
                    latLon.Latitude latLon.Longitude
        // Type inference deduces that the anonymous record being
        // instantiated here is a struct.
        // "Latitude: 51.495, Longitude: -0.144"
        printfn "%s"
            (formatLatLon {| Latitude = 51.4952; Longitude = -0.1441 |})
Listing 7-32

Structural anonymous records

Anonymous Records and C#

From C#’s point of view, F# anonymous records look like C#’s anonymous types. If a C# caller requires an anonymous type, feel free to give it an anonymous record as your return value. More commonly, if an F# caller is calling a C# API that requires an anonymous type, you can give it an anonymous record instance.

Pattern Matching on Anonymous Records

Finally, we come to something which anonymous records don’t support! You can’t pattern match on them. If we attempt to adapt the code from Listing 6-11 in the previous chapter, we find that it doesn’t compile when anonymous records are used (Listing 7-33).

Given how lightweight named records are anyway, this is hardly a major hardship. However, it’s worth noting that there is an open language design suggestion to add a degree of pattern matching for anonymous records, so it’s possible that code like Listing 7-33 may be possible in future.
      let songs =
         [ {| Id = 1
              Title = "Summertime"
              Artist = "Ray Barretto"
              Length = 99 |}
           {| Id = 2
              Title = "La clave, maraca y guiro"
              Artist = "Chico Alvarez"
              Length = 99 |}
           {| Id = 3
              Title = "Summertime"
              Artist = "DJ Jazzy Jeff & The Fresh Prince"
              Length = 99 |} ]
      // Doesn't compile:
      let formatMenuItem ( {| Title = title; Artist = artist |} ) =
          let shorten (s : string) = s.Substring(0, 10)
          sprintf "%s - %s" (shorten title) (shorten artist)
Listing 7-33

You cannot pattern match on anonymous records

Adding Methods to Anonymous Records

You cannot directly add methods to anonymous records. There are workarounds for this, but I can’t think of a reason you would do such a thing, given how much it obfuscates your code. I rarely even add methods to named records!

Mutation and Anonymous Records

You can’t declare an anonymous record with a mutable field, though again there is an open language suggestion to address this. Again, I rarely if ever have mutable fields in a named record. Having one in an anonymous record seems even more inadvisable. You can declare entire anonymous record instances as mutable, but I am hard-pressed to think of a situation where you would want to do so.

Record Layout

I cover spacing and layout in general in Chapter 13, but there are few code formatting points that are specific to record types (both named and anonymous).
  • Use Pascal case for both record type names and for the individual field labels. All the listings in this chapter follow that approach.

  • Where a record type definition or instantiation doesn’t fit comfortably into a single line, break it into multiple lines, leftaligning the field labels. If you put fields on separate lines, omit the separating semicolons. Don’t mix single and multiline styles (Listing 7-34).

  • Use the field names in the same order in the record type definition as in any instantiations and with operations.

        // Declaration:
        // Good
        type LatLon1 = { Lat : float; Lon : float }
        // Good
        type LatLon2 =
            { Latitude : float
              Longitude : float }
        // Good
        type LatLon3 = {
            Latitude : float
            Longitude : float }
        // Bad - needless semi-colons
        type LatLon4 = {
            Latitude : float;
            Longitude : float }
        // Bad - mixed newline style
        type Position = { Lat : float; Lon : float
                          Altitude : float }
        // Instantiation:
        // Good
        let ll1 = { Lat = 51.9723; Lon = 1.1497 }
        // Good
        let ll2 =
            { Latitude = 51.9723
              Longitude = 1.1497 }
        // Bad - needless semi-colons
        let ll3 =
            { Latitude = 51.9723;
              Longitude = 1.1497 }
        // Bad - mixed newline style
        let position = { Lat = 51.9723; Lon = 1.1497
                         Altitude = 22.3 }
Listing 7-34

Good and bad record type layout

Recommendations

Here are my suggestions to help you make great code with record types:
  • Prefer records to class types unless you need the internal and external representations of data to differ, or the type needs to have “moving parts” internally.

  • Think long and hard before making record fields or (worse still!) whole records mutable; instead, get comfortable using copy-and-update record expressions (i.e., the with keyword).

  • Make sure you understand the importance of “structural” (content) equality in record types, but make sure you also know when it would be violated. (When a field doesn’t itself, have content equality.)

  • Sometimes, it’s useful to add instance methods, static methods, or overrides to record types, but don’t get carried away: having to do this, a lot might indicate that a class type would be a better fit.

  • Consider putting record types on the stack with [<Struct>] if this gives you performance benefits across the whole life cycle of the instance.

  • Lay your record type definitions and instantiations out carefully and consistently.

Next, some recommendations specific to anonymous records:
  • Consider anonymous records where the scope of the instances you create is narrow; a few lines or at most one module or source file. Pipelines that use tuples to pass values between their stages are a particularly attractive target. If the type is more pervasive, it’s probably better to declare a named record up front.

  • Obviously, don’t use anonymous records if one of their shortcomings is going to force you into strange workarounds. For instance, if you need to pattern match on records, anonymous records are currently a nonstarter. Likewise, you won’t get far in adding methods to an anonymous record, and the workarounds to this aren’t, in my opinion, particularly useful.

  • Although you can use anonymous records in type hints, I’m not convinced that you should do so. It leads to some pretty strange function headers, and in general, these look much simpler if done in terms of named record types declared separately.

  • Don’t discount the cognitive benefits of declaring a named record up front. When you name a record type, you are making a focused statement about what kind of thing you want to create and work with. If you are clear about that, a lot of the code that instantiates and processes instances of the record type will naturally “fall out” of that initial statement of intent.

  • Anonymous records are usually the best solution when interacting with C# code that produces or consumes anonymous objects.

Summary

Effective use of records is core to writing great F# code. It’s certainly my go-to data structure when I want to store small groups of labeled values. I only switch to classes (Chapter 8) when I find that I’m adorning my record types to the extent they might as well be classes – which is rarely. And any day I find that I’m using the with keyword with record types is a good day! I often use anonymous records to clarify code where, in earlier versions of F#, I might have used tuples.

All that said – classes have their place, even in F# code, so in the next chapter, we’ll talk about them in considerable detail.

Exercises

Exercise 7-1 – Records and Performance

You need to store several million items, each consisting of X, Y, and Z positions (single precision) and a DateTime instance. For performance reasons, you want to store them on the stack.

How might you model this using an F# record?

How can you prove, in the simple case, that instantiating a million records works faster when the items are placed on the stack than when they are allowed to go on the heap?

Exercise 7-2 – When To Use Records

You have an idea for a novel cache that stores expensive-to-compute items when they are first requested and periodically evicts the 10% of items that were least accessed over a configurable time period. Is a record a suitable basis for implementing this? Why or why not?

Don’t bother to actually code this – it’s just a decision-making exercise.

Exercise 7-3 – Equality and Comparison
A colleague writes a simple class to store music tracks but is disappointed to discover that they can’t deduplicate a list of tracks by making a Set instance from them:
        type Track (name : string, artist : string) =
            member __.Name = name
            member __.Artist = artist
        let tracks =
            [ Track("The Mollusk", "Ween")
              Track("Bread Hair", "They Might Be Giants")
              Track("The Mollusk", "Ween") ]
            // Error: The type 'Track' does not support the
            // comparison constraint
            |> Set.ofList

What’s the simplest way to fix the problem?

Exercise 7-4 – Modifying Records
Start off with the struct record from Exercise 7-1. Write a function called translate that takes a Position record and produces a new instance with the X, Y, and Z positions altered by specified amounts, but the Time value unchanged.
        open System
        [<Struct>]
        type Position = {
            X : float32
            Y : float32
            Z : float32
            Time : DateTime }
Exercise 7-5 – Anonymous Records

How would you alter the solution to Exercise 7-1 so that you return an array of struct anonymous records instead of struct named records? What effect does doing this have on performance?

Exercise Solutions

Exercise 7-1 – Records and Performance
You need to create a record type with suitably typed fields X, Y, Z, and Time. Mark the record with the [<Struct>] attribute to force instances to be placed on the stack. Note that DateTime is also a value type (struct) so the Time field should not interfere with the storage.
        open System
        [<Struct>]
        type Position = {
            X : float32
            Y : float32
            Z : float32
            Time : DateTime }
You can do a simple performance check by starting a “stopwatch” and checking its ElapsedMilliseconds property.
        let sw = System.Diagnostics.Stopwatch.StartNew()
        let test =
            Array.init 1_000_000 (fun i ->
                { X = float32 i
                  Y = float32 i
                  Z = float32 i
                  Time = DateTime.MinValue } )
        sprintf "%ims" sw.ElapsedMilliseconds

On my system, the instantiation took around 40ms with the [<Struct>] attribute and around 50ms without it. In reality, you’d need to check the whole life cycle of the items (instantiation, access, and release) in the context of the real system and volumes you were working on.

Exercise 7-2 – When To Use Records

This sounds like something with a number of moving parts, including storage for the cached items, a timer for periodic eviction, and members allowing values to be retrieved independently of how they are stored internally. There is also, presumably, some kind of locking going on for thread safety. This clearly fulfills the criteria of “internal storage differs from external representation” and “has moving parts,” which means that one or more class types are almost certainly a more suitable approach than a record type.

Exercise 7-3 – Equality and Comparison
Simply change the class type to a record type. Now your type will have structural (content) equality, and Set.ofList can be used successfully to deduplicate a collection of tracks.
        type Track = {
            Name : string
            Artist : string }
        // set [{Name = "Bread Hair";
        //       Artist = "They Might Be Giants";};
        //      {Name = "The Mollusk";
        //       Artist = "Ween";}]
        let tracks =
            [ { Name = "The Mollusk"
                Artist = "Ween" }
              { Name = "Bread Hair"
                Artist = "They Might Be Giants" }
              { Name = "The Mollusk"
                Artist = "Ween" } ]
            |> Set.ofList
Exercise 7-4 – Modifying Records
Use the with keyword to assign new values for the X, Y, and Z values. Note that you can access the old values from the original instance using dot notation. Make sure you have the instance to be “modified” as the last function parameter, to make your function pipeline friendly.
open System
[<Struct>]
type Position = {
    X : float32
    Y : float32
    Z : float32
    Time : DateTime }
let translate dx dy dz position =
    { position with
        X = position.X + dx
        Y = position.Y + dy
        Z = position.Z + dz }
let p1 =
    { X = 1.0f
      Y = 2.0f
      Z = 3.0f
      Time = DateTime.MinValue }
// { X = 1.5f;
//   Y = 1.5f;
//   Z = 4.5f;
//   Time = 01/01/0001 00:00:00;}
p1 |> translate 0.5f -0.5f 1.5f
Exercise 7-5 – Anonymous Records
You can do this by using the keyword struct and instantiating an anonymous record with the appropriate field names and values between {| |}:
open System
let sw = System.Diagnostics.Stopwatch.StartNew()
let test =
    Array.init 1_000_000 (fun i ->
        struct
            {| X = float32 i
               Y = float32 i
               Z = float32 i
               Time = DateTime.MinValue |} )
sprintf "%ims" sw.ElapsedMilliseconds

In this simple case I couldn’t see any consistent difference between this version and a named record version.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.82.252