Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Record vs Class

Tags:

types

oop

f#

I used to think of a Record as a container for (immutable) data, until I came across some enlightening reading.

Given that functions can be seen as values in F#, record fields can hold function values as well. This offers possibilities for state encapsulation.

module RecordFun =

    type CounterRecord = {GetState : unit -> int ; Increment : unit -> unit}

    // Constructor
    let makeRecord() =
        let count = ref 0
        {GetState = (fun () -> !count) ; Increment = (fun () -> incr count)}

module ClassFun =

    // Equivalent
    type CounterClass() = 
        let count = ref 0
        member x.GetState() = !count
        member x.Increment() = incr count

usage

counter.GetState()
counter.Increment()
counter.GetState()

It seems that, apart from inheritance, there’s not much you can do with a Class, that you couldn’t do with a Record and a helper function. Which plays better with functional concepts, such as pattern matching, type inference, higher order functions, generic equality...

Analyzing further, the Record could be seen as an interface implemented by the makeRecord() constructor. Applying (sort of) separation of concerns, where the logic in the makeRecord function can be changed without risk of breaking the contract, i.e. record fields.

This separation becomes apparent when replacing the makeRecord function with a module that matches the type’s name (ref Christmas Tree Record).

module RecordFun =

    type CounterRecord = {GetState : unit -> int ; Increment : unit -> unit}

    // Module showing allowed operations 
    [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
    module CounterRecord =
        let private count = ref 0
        let create () =
            {GetState = (fun () -> !count) ; Increment = (fun () -> incr count)}

Q’s: Should records be looked upon as simple containers for data or does state encapsulation make sense? Where should we draw the line, when should we use a Class instead of a Record?

Note the model from the linked post is pure, whereas the code above is not.

like image 953
Funk Avatar asked Dec 31 '16 17:12

Funk


2 Answers

I do not think there is a single universal answer to this question. It is certainly true that records and classes overlap in some of their potential uses and you can choose either of them.

The one difference that is worth keeping in mind is that the compiler automatically generates structural equality and structural comparison for records, which is something you do not get for free for classes. This is why records are an obvious choice for "data types".

The rules that I tend to follow when choosing between records & classes are:

  • Use records for data types (to get structural equality for free)
  • Use classes when I want to provide C#-friendly or .NET-style public API (e.g. with optional parameters). You can do this with records too, but I find classes more straightforward
  • Use records for types used locally - I think you often end up using records directly (e.g. creating them) and so adding/removing fields is more work. This is not a problem for records that are used within just a single file.
  • Use records if I need to create clones using the { ... with ... } syntax. This is particularly nice if you are writing some recursive processing and need to keep state.

I don't think everyone would agree with this and it is not covering all choices - but generally speaking, using records for data and local types and classes for the rest seems like a reasonable method for choosing between the two.

like image 186
Tomas Petricek Avatar answered Oct 21 '22 06:10

Tomas Petricek


If you want to achieve data hiding in a record, I feel there are better ways of going about it, like abstract data type "pattern".

Take a look at this:

type CounterRecord = 
    private { 
        mutable count : int 
    }
    member this.Count = this.count
    member this.Increment() = this.count <- this.count + 1
    static member Make() = { count = 0 }
  • The record constructor is private, so the only way of constructing an instance is through the static Make member,
  • count field is mutable - not something to be proud about, but I'd say fair game for your counter example. Also it's not accessible from outside the module where it's defined due to private modifier. To access it from outside, you have the read-only Count property.
  • Like in your example, there's an Increment function on the record that mutates the internal state.
  • Unlike your example, you can compare CounterRecord instances using auto-generated structural comparisons - as Tomas mentioned, the selling point of records.

As for records-as-interfaces, you might see that sometimes in the field, though I think it's more of a JavaScript/Haskell idiom. Unlike those languages, F# has the interface system of .NET, made even stronger when coupled with object expressions. I feel there's not much reason to repurpose records for that.

like image 6
scrwtp Avatar answered Oct 21 '22 06:10

scrwtp