In the simplest case, consider that I use the analogy of set product to combine types A
and B
; the result would be a set of data pairs where the first pair constituent is of type A
, the second constituent is of type B
, and the whole combination is a Cartesian product of A and B.
F# offers two product algebraic data types, that is, tuples and records.
I have already touched tuples in previous chapters; now I'll go deeper into this subject.
A tuple is a combination of two or more values of any type. The tuple value element type can be of anything: primitive types, other tuples, custom classes, and functions. For example, take a look at the following code line (Ch5_1.fsx
):
let tuple = (1,"2",fun() ->3)
This represents a tuple assembled from three elements of type int* string * (unit -> int)
.
In order to belong to the same type of tuple, two tuple values must have the same number of elements with the similar types in the order of occurrence.
F# automatically implements the structural equality for tuples if each element type supports the equality constraint. Tuples are equal if all their elements are equal pairwise as shown in the following code (Ch5_1.fsx
):
let a = 1, "car" a = (1, "car")
The preceding equality expression value is true
. However, for the value of tuple
bound above the following expression does not compile (Ch5_1.fsx
):
tuple = (1,"2",fun() ->3)
The compiler complains that the (unit -> int)
type, which is the function forming the third element of the tuple, does not support the 'equality'
constraint. The equality relationship is not defined for the F# function values.
Structural comparison for tuples is similarly provided by F# out of the box and is based on pairwise comparisons of elements in a lexicographical order from left to right given that all element types fulfill the 'comparison'
constraint as shown in the following code (Ch5_1.fsx
):
a < (2,"jet")
The preceding expression value is true
.
This chapter is the perfect place to keep the promise I made in Chapter 4, Basic Pattern Matching regarding pattern matching in the capacity of the data structure disassembling tool. The following code snippet demonstrates how value binding can carry the functionality of pattern matching outside of the match
construction (Ch5_1.fsx
):
let (elem1, elem2) = a printfn "(%i,%s)" elem1 elem2
Here, elem1
and elem2
effectively acquire values of the first and second elements of tuple a
, which is reflected by the (1,car)
output.
Elements of a tuple that are of no interest within a particular tuple disassemble pattern may be omitted using the familiar match-all _
template, as shown in the following code (Ch5_1.fsx
):
let (_,_,f) = tuple in f()
This snippet highlights how to obtain and invoke a function extracted from the third element of the tuple value; the first two tuple elements are simply ignored with the help of the _
template.
The tuple type does not have an explicit name. This fact effectively makes normal F# type augmentation impossible. Nevertheless, there is still some space left for a good hack. This one exploits the need to have interop with other .NET languages.
Documentation (https://msdn.microsoft.com/en-us/library/dd233200.aspx) states that the compiled form of a tuple represents the corresponding overload of class Tuple (https://msdn.microsoft.com/en-us/library/system.tuple.aspx). Given this fact, I can augment the compiled presentation and apply the augmented method using the cast, as shown in the following code (Ch5_1.fsx
):
let a = 1,"car" type System.Tuple<'T1,'T2> with member t.AsString() = sprintf "[[%A]:[%A]]" t.Item1 t.Item2 (a |> box :?> System.Tuple<int,string>).AsString()
Here, I have augmented a tuple of two generic elements that have type System.Tuple<'T1,'T2> with
the AsString
instance method, which allows a very distinctive presentation of the tuple value. Then, given the instance of the int*string
tuple, I have upcasted it to obj
type with the box
function and then immediately downcasted it with :?>
operator to System.Tuple<int,string>
type, followed by calling the AsString
augmented method on the deceivingly constructed System.Tuple<int,string>
class instance, getting the expected result, that is, [[1]:["car"]]
.
Wrapping it up, I can conclude that tuples represent a simple algebraic data type that fits simple designs well. Using tuples instead of custom types for data composition is archetypal for idiomatic F# usage.
Records represent the other F# native product algebraic data type. It addresses the matter that exceptional simplicity of tuples causing some deficiencies. The most unfavorable feature of tuples is the lack of binding of a tuple to a concrete kind of otherwise structurally similar tuple type. For the F# compiler, there is no difference between (1,"car")
and (10,"whiskey")
, which puts the burden of distinguishing the instance type upon the programmer. Would it be nice to supply structurally similar but semantically different types with explicit names? Also it would be helpful to label tuple constituents with unique names in order to stop relying just on the element position? Sure, welcome to F# records!
F# records may be considered as the tuples of explicitly named types with labeled elements. Referring to the tuple sample given in the preceding script Ch5_1.fsx
, it can be rewritten as follows (Ch5_2.fsx
):
type transport = { code: int; name: string } let a = { code = 1; name = "car" }
After placing the preceding snippet into FSI, you get the result shown in the following screenshot:
The preceding screenshot visually demonstrates the benefits of records over tuples when it comes to the unambiguous labeling of the whole and its parts.
Interestingly, the naming of record fields makes it unnecessary to stick to a certain order of field listing as shown in the following code (Ch5_2.fsx
):
let b = { name = "jet"; code = 2 }
Without any problems, value b
is recognized as a binding of type transport
.
After being constructed, F# records are genuinely immutable, similar to tuples. The language provides just another form of record construction off the existing instance using the with
modifier as shown in the following code (Ch5_2.fsx
):
let c = { b with transport.name = "plane" }
This translates into an instance of transport { code = 2; name = "plane" }
. Note the use of the "fully qualified" field name, transport.name
. I put it this way in order to highlight how it can be possible to resolve ambiguity as different record types may have similarly named fields.
No surprises here. F#, by default, provides structural equality and comparison for records in a manner similar to tuples. However, having an explicit type declaration allows more flexibility in this matter.
For example, if structural equality is not desired and reference equality is required for any reason, it is not a problem for records, which type definition may be decorated with [<ReferenceEquality>]
attribute as shown in the following code snippet (Ch5_2.fsx
):
[<ReferenceEquality>] type Transport = { code: int; name: string } let x = {Transport.code=5; name="boat" } let y = { x with name = "boat"} let noteq = x = y let eq = x = x
The following screenshot illustrates what happens if running this code in FSI:
Note that after decorating the Transport
type with the ReferenceEquality
attribute, two structurally equal records, x
and y
, are not considered equal anymore.
It is worth noting that decorating a record type with the [<CLIMutable>]
attribute makes the underlying record a standard mutable .NET CLI type for interoperability scenarios; in particular providing additionally a default parameterless constructor and elements mutability. See Core.CLIMutableAttribute Class (F#) (https://msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/core.climutableattribute-class-%5Bfsharp%5D) for further details.
Disassembling records with pattern matching is similar to the disassembling tuples and may work with or without the match
construction. The latter case is preferable from the standpoint of succinctness as shown in the following code (Ch5_2.fsx
):
let { transport.code = _; name = aName } = a
This discards the code
field of a
as not interesting and binds its name
field with the aName
value. The same effect can be achieved with even shorter code:
let { transport.name = aname} = a
If a single field value is required, then simple let
aName' = a.name
works too.
Having an explicit type declaration for F# records allows a great deal of augmenting around. A nice example of augmenting a record type in order to implement a thread safe mutable singleton property can be found in the SqlClient Type provider code (https://github.com/fsprojects/FSharp.Data.SqlClient/blob/c0de3afd43d1f2fc6c99f0adc605d4fa73f2eb9f/src/SqlClient/Configuration.fs#L87). A distilled snippet is represented as follows (Ch5_3.fsx
):
type Configuration = { Database: string RetryCount: int } [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] [<AutoOpen>] module Configuration = let private singleton = ref { Database = "(local)"; RetryCount = 3 } let private guard = obj() type Configuration with static member Current with get() = lock guard <| fun() -> !singleton and set value = lock guard <| fun() -> singleton := value printfn "Default start-up config: %A" Configuration.Current Configuration.Current <- { Configuration.Current with Database = ".SQLExpress" } printfn "Updated config: %A" Configuration.Current
Here, Database
and RetryCount
are kept as fields of the F# record that is placed as a thread safe static property backed by the singleton
private reference. The beauty of the pattern is that at any moment, configuration can be changed programmatically at the same time keeping the singleton thread safe.