Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When should I use a record over a tuple?

Tags:

f#

I am trying to practice Domain Driven Design in F# and stumbled across the following question:

Why would I use a record when using a tuple appears to require less syntax and appears to be more powerful in regards to pattern matching and just overall usage scenarios?

For example, I feel as if I no longer have to create a record type just to implement a discriminated union if I were to use a tuple instead.

type Name = { First:string 
              Middle:string option
              Last:string }

type Duration = { Hours:int
                  Minutes:int
                  Seconds:int }

type Module = 
    | Author of Name
    | Title of string
    | Duration of Duration

let tryCreateName (first, middle, last) =
    { First=first; Middle=Some middle; Last=last }

let tryCreateDuration (hours, minutes, seconds) =
    { Hours=hours; Minutes=minutes;Seconds=seconds }

let name = tryCreateName ("Scott", "K", "Nimrod")

let hours = 1
let minutes = 30
let seconds = 15

let duration = tryCreateDuration (hours, minutes, seconds)

Are my thoughts accurate?

Are tuples desired over records in most scenarios?

like image 733
Scott Nimrod Avatar asked Jan 18 '16 13:01

Scott Nimrod


People also ask

What is the difference between a record and a tuple?

Records are similar to tuples, in that they group together various data elements. A record has fields, and these are named. While tuples are an ordered collection of data values, a tuple is an unordered collection of labeled data values.

What is a record in JavaScript?

A record is analogous to an Object in JavaScript with the exception that the Record is not an Object but a deeply immutable primitive value. Likewise, a Tuple is like an Array but is a deeply immutable primitive value.


2 Answers

For Domain Modelling, I'd recommend using types with named elements; that is, records, discriminated union, and perhaps the occasional class or interface.

Structurally, records and tuples are similar; in algebraic data type parlance, they're both product types.

The difference is that with tuples, the order of values matter, and the role of each element is implicit.

> (2016, 1, 2) = (2016, 1, 2);;
val it : bool = true
> (2016, 1, 2) = (2016, 2, 1);;
val it : bool = false

In the above example, you may guess that these tuples model dates, but which ones exactly? Is it the second of January 2016? Or is it the first of February 2016?

With records, on the other hand, the order of elements don't matter, because you bind them, and access them, by name:

> type Date = { Year : int; Month : int; Day : int };;

type Date =
  {Year: int;
   Month: int;
   Day: int;}

> { Year = 2016; Month = 1; Day = 2 } = { Year = 2016; Day = 2; Month = 1 };;
val it : bool = true

It's also clearer when you want to pull out the constituent values. You can easily get the year from a record value:

> let d = { Year = 2016; Month = 1; Day = 2 };;

val d : Date = {Year = 2016;
                Month = 1;
                Day = 2;}

> d.Year;;
val it : int = 2016

It's much harder to pull values out of a tuple:

> let d = (2016, 1, 2);;

val d : int * int * int = (2016, 1, 2)

> let (y, _, _) = d;;

val y : int = 2016

Granted, for pairs, you can use the built-in functions fst and snd to access the elements, but for tuples with three or more elements, you can't easily get at the values unless you pattern match.

Even if you define custom functions, the role of each element is still implicitly defined by its ordinal. It's easy to get the order of values wrong if they're of the same type.

So for Domain Modelling, I always prefer explicit types, so that it's clear what's going on.

Are tuples never appropriate, then?

Tuples are useful in other contexts. When you need to use an ad hoc type in order to compose functions, they are more appropriate than records.

Consider, as an example, Seq.zip, which enables you to combine two sequences:

let alphalues = Seq.zip ['A'..'Z'] (Seq.initInfinite ((+) 1)) |> Map.ofSeq;;

val alphalues : Map<char,int> =
  map
    [('A', 1); ('B', 2); ('C', 3); ('D', 4); ('E', 5); ('F', 6); ('G', 7);
     ('H', 8); ('I', 9); ...]

> alphalues |> Map.find 'B';;
val it : int = 2

As you can see in this example, tuples are only a step towards the actual goal, which is a map of alphabetical values. It'd be awkward if we had to define a record type whenever we wanted to compose values together inside of an expression. Tuples are well-suited for that task.

like image 60
Mark Seemann Avatar answered Sep 18 '22 02:09

Mark Seemann


You may be interested in optional parameters, which are only permitted on types. To avoid changing the order of your arguments, I've made the last name optional too (perchance it also models your problem domain better, e.g. when encountering Mononyms).

To represent a difference between two points in time there is already a library function in the framework, System.TimeSpan.

Your example could therefore be written as

type Name = {
    FirstName   : string 
    MiddleName  : string option
    LastName    : string option } with
    static member Create(firstName, ?middleName, ?lastName) =
        {   FirstName   = firstName
            MiddleName  = middleName
            LastName    = lastName }

type System.TimeSpan with
    static member CreateDuration(hours, ?minutes, ?seconds) = 
        System.TimeSpan(
            hours,
            defaultArg minutes 0,
            defaultArg seconds 0 )

let name = Name.Create("Scott", "K", "Nimrod")
let duration = System.TimeSpan.CreateDuration(1, 30, 15)

The idea is to provide extension methods that take tupled arguments of varying length, and return a complex data structure, e.g. a record or a struct.

like image 33
kaefer Avatar answered Sep 18 '22 02:09

kaefer