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?
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With