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.