Product algebraic data types

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.

Tuples

I have already touched tuples in previous chapters; now I'll go deeper into this subject.

Tuple composition

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.

Tuple equality and comparison

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.

Tuple decomposition with pattern matching

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.

Tuple augmentation

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

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!

Record composition

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:

Record composition

Defining the F# record type and instance

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.

Record equality and comparison

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:

Record equality and comparison

Referential equality for F# records

Note that after decorating the Transport type with the ReferenceEquality attribute, two structurally equal records, x and y, are not considered equal anymore.

Note

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.

Record decomposition with pattern matching

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 letaName' = a.name works too.

Record augmentation

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset