United Kingdom: +44 (0)208 088 8978

Are Interfaces and Records the same thing?

Isaac compares two features of the F# language, Interfaces and Records, and highlights a key difference between them.

We're hiring Software Developers

Click here to find out more

One of F#'s core type system elements is the Record - an optimised way of working with immutable data that has structural equality:

type Person = {
    Name: string
    Age: int
}

let isaac = { Name = "Isaac"; Age = 21 } // create a Person
let doppleganger = { Name = "Isaac"; Age = 21 } // create another Person
let areTheyTheSame = (isaac = doppleganger) // true, all values are the same
fooRec.Name <- "Fred" // compiler error, This field is not mutable

Another language feature of F# - and of .NET in general - is the Interface. An Interface is used for specifying a contract, typically for behaviour (but also occasionally for data):

type IPerson =
    abstract member Name: string // some data fields
    abstract member Age: int
    abstract member Greet: name: string -> string // a behaviour

Interestingly, despite Interfaces being more of a ".NET" than an "F#-specific" feature, F# has very good support for utilising Interfaces, including the use of Object Expressions to rapidly create an instance of an Interface without first formally defining a type:

let isaacInterface =
    { new IPerson with // providing an implementation for IFoo without creating a type!
        member _.Name = "Isaac"
        member _.Age = 21

        member this.Greet name =
            $"Hello, {name}! My name is {this.Name}."
    }

Records as behaviour holders

Although we tend to think of Records as "data holders", you can also use Records to expose behaviours as well (this is incidentally how the Fable Remoting library works). In other words, we treat a Record as a store of functions, rather than simple data fields:

type ICustomerApi = {
    LoadCustomer: string -> Person
    SaveCustomer: Person -> unit
    TryFindCustomer: string -> Person option
    DeleteCustomer: string -> unit
}

let customerApi = {
    LoadCustomer = fun name -> { Name = "Prash"; Age = 42 }
    SaveCustomer = fun p -> () // save logic elided...
    TryFindCustomer = fun name -> None
    DeleteCustomer = fun name -> () // delete logic elided...
}

This is not especially unusual in a functional programming language insofar as functions are considered just another form of data that can be passed around.

Interestingly, Records can also implement interfaces, similar to how Classes can.

Generics - a key differentiator

So if we can treat Records as a list-of-functions, and Records have superior support for type inference over Interfaces, why might you want to use Interfaces instead of Records? The answer is when we add generics into the mix: Interfaces allow you to create generic instance methods on a non-generic Interface:

type IPerson = // no generic argument needed here...
    abstract member Name: string
    abstract member Age: int
    abstract member Greet: 'T -> string // generic argument allowed here

This isn't possible with Records:

type Person<'T> = { // must have a generic argument at the type level.
    Name: string
    Age: int
    Greet: 'T -> string // generic argument on this function
}

Why might this be useful? Because a single generic method can act on multiple types:

let isaac : IPerson =
    { new IPerson with // no generic argument required
        ...
        member this.Greet other = $"Hello, {other}!"
    }

let greetLotsInt (p: IPerson) =
    let x = p.Greet "Prash" // called with a string
    let y = p.Greet 123 // called with an int
    $"{x}. {y}"

This sort of behaviour isn't possible with a Record as a function argument - a function will constrain the supplied argument to a single generic argument:

let greetLots (p:Person<_>) = // "open" generic argument is inferred to be string based on usage below
    let x = p.Greet "Prash"
    let y = p.Greet 123 // This expression was expected to have type 'string' but here has type 'int'
    $"{x}. {y}"

Summary

Using generic code is a complex area of .NET in general, and it allows many powerful abstractions. Support for generics within F#-specific constructs (such as functions and Records) is excellent - and Statically Resolved Type Parameters take this up a level. However, the humble Interface (a CLR feature since the .NET 1.0) also has good support for generics and can even provide capabilities that Records cannot.