12
votes

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?

2
I would answer but I know Tomas will do better. IIRC it has more to do with working with C# and .NET. If you are purely in F# then go with the tuple. If you are working with other .NET code then go with the record. There are other factors such as if it is large go with a record, if it not public go with a tuple, etc. Also before you ask also see When to Use Classes, Unions, Records, and Structures at the end of the link.Guy Coder
Have you seen all the info in the F# tag?Guy Coder
@ Guy Coder - Thanks for the links. The Guidelines appear to support your guidance on why records would be used. Thanks.Scott Nimrod

2 Answers

17
votes

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.

2
votes

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.