Construction and deconstruction of F# values

Posted March 1, 2021 by Rafał Gwoździński ‐ 8 min read

Construction and deconstruction of F# values

Simple type deconstruction

Let's say we have a simple type (aka Single case discriminated union type) that wraps an int.

type SimpleInt = SimpleInt of int
let simpleInt = SimpleInt 42

We have created a value and bound it to simpleInt variable. How can we retrieve it's content?

Anonymous function deconstruction

We can deconstruct SimpleInt in lambda expression

let intValue = simpleInt |> fun (SimpleInt s) -> s

Let binding deconstruction

We can deconstruct it in let assignment

let (SimpleInt intValue') = simpleInt

Dedicated value retrieving function

We can add a module with the type name that contains deconstructing function. This is a bit more verbose, but can be handy, especially when we use it in many places and are tired of redundant lambdas.

module SimpleInt =
    let value (SimpleInt s) = s

let intValue'' = simpleInt |> SimpleInt.value

Direct deconstruction of function parameter

We can also unwrap value directly in a function that uses it.

module SimpleIntAdder =
    let addSimpleInts (SimpleInt i1) (SimpleInt i2) = 
        SimpleInt (i1 + i2)

Parallel between construction and deconstruction

There is a parallel between notation of construction and deconstruction of simple types. In both cases they are written as ({Type Name} {Value})

let wrapped = (SimpleInt 42)
let (SimpleInt unwrapped) = wrapped

Record types

Now, let's see what happens with records. Let's define and construct a value for a simple record type.

type SimpleRecord = 
    { First: SimpleInt
      Second: int }
let simpleRecord = 
    { First = SimpleInt 42
      Second = 1 }

Deconstruction similar to simple types

We can deconstruct record in:

  • lambda expression:
// Remark: Added type annotations to r1, r2 for clarity
let (r1: SimpleInt), (r2: int) = 
    simpleRecord 
    |> fun {First = x; Second = y} -> (x, y)
  • let assignment:
let {First = r1'; Second = r2'} = simpleRecord
  • function parameter:
// Remark: We deconstruct both record and it's internal field (which is of type SimpleInt)
module SimpleRecordAdder =
    let addRecordFields {First = (SimpleInt f); Second = s} = f + s

There is a parallel between construction and deconstruction of records similar to simple types:

let wrappedRecord = {First = SimpleInt 42; Second = 1}
let {First = (SimpleInt f); Second = s} = wrappedRecord 

Partial deconstruction

If we don't need all of record fields, we can use partial deconstruction in all previously defined cases.

let {Second = s'} = wrappedRecord
let s'' = wrappedRecord |> fun {Second = x} -> x

module SecondFieldAdder =
    let addOneToSecondField {Second = s} = s + 1

let {First = (SimpleInt f')} = wrappedRecord
let f'' = wrappedRecord |> fun {First = (SimpleInt x)} -> x

module FirstFieldAdder =
    let addOneToFirstField {First = (SimpleInt f)} = f + 1

Pattern matching

Pattern matching is one of the most popular usage of deconstruction.

Simple type

type MyInt = MyInt of int
let myVal = MyInt 42

let valueIfNotZero = 
    match myVal with 
    | MyInt 0 -> None
    | MyInt x -> Some x

Record type

We can match on all record fields:

type MyRecord = { First: int; Second: int }
let myRec = {First = 1; Second = 2}

// Full
let addIfNotZeros =
    match myRec with
    | {First = 0; Second = 0} -> None
    | {First = x; Second = y} -> Some (x + y)

Or we can use partial deconstruction

let getSecondIfNotZero =
    match myRec with
    | {Second = 0} -> None
    | {Second = x} -> Some x

List

Lists offer a refined ways of deconstruction. Let's define a simple list and see how we can match on it.

let myList = [1;2;3;4;5]

Exact match

let is12345 =
    match myList with
    | [1;2;3;4;5] -> true
    | _ -> false

Match exact number of items

let sum5OrZero =
    match myList with
    | [a;b;c;d;e] -> a+b+c+d+e
    | _ -> 0

Match head/tail

We can match head/tail with h::t construct. Head is the first element of the list, Tail is a list of all further elements.

Example use cases:

  • Exact head
let is1First =
    match myList with
    | 1::t -> true
    | _ -> false
  • Exact tail
let is2345Tail =
    match myList with
    | h::[2;3;4;5] -> true
    | _ -> false
  • Deconstruct head
let head =
    match myList with
    | h::t -> Some h
    | _ -> None
  • Deconstruct tail
let skipHead =
    match myList with
    | h::t -> t
    | _ -> []
  • Empty or not?
let isEmpty =
    match myList with
    | [] -> true 
    | _ -> false
  • One element
let isSingle =
    match myList with
    | [x] -> true
    | _ -> false

Arrays

An Array structure offers fewer ways of matching than list.

We construct an array as:

let myArray = [|1;2;3;4;5|]

Cases similar to List type

  • Exact match
let is12345' =
    match myArray with
    | [|1;2;3;4;5|] -> true
    | _ -> false
  • Deconstruct exact number of items
let sum5OrZero' =
    match myArray with
    | [|a;b;c;d;e|] -> a+b+c+d+e
    | _ -> 0
  • Empty?
let isEmpty' =
    match myArray with
    | [||] -> true
    | _ -> false
  • One element
let isSingle' =
    match myArray with
    | [|x|] -> true
    | _ -> false

Head/tail

Unfortunately, there is no built-in way to match h::t on arrays in F#.

Example Code

To get a better feel for concepts described in this post, I encourage readers to try this code on their own machines. The easiest way is to put it into fsx script and run using REPL.

////////////////////////////////////
//// Simple type deconstruction ////  
////////////////////////////////////

type SimpleInt = SimpleInt of int
let simpleInt = SimpleInt 42

// We created a value in a usual way and bound it to `simpleInt` variable.
// How can we retrieve a value from it?

// *Anonymous function deconstruction
//  We can deconstruct `SimpleInt` in lambda expression
let intValue = simpleInt |> fun (SimpleInt s) -> s

// *Let binding deconstruction
//  We can deconstruct it in `let` assignment
let (SimpleInt intValue') = simpleInt

// *Dedicated value retrieving function 
//  We can add a module with the type name that contains deconstructing function.
//  This is a bit more verbose, but can be handy, especially when we use it in many places and are tired of redundant lambdas.
module SimpleInt =
    let value (SimpleInt s) = s

let intValue'' = simpleInt |> SimpleInt.value

// *Direct deconstruction of function parameter
//  We can also unwrap value directly in a function that uses it.
module SimpleIntAdder =
    let addSimpleInts (SimpleInt i1) (SimpleInt i2) = 
        SimpleInt (i1 + i2)

//// Parallel between construction and deconstruction
// There is a parallel between notation of construction and deconstruction of simple types.
// In both cases they are written as `({Type Name} {Value})`
let wrapped = (SimpleInt 42)
let (SimpleInt unwrapped) = wrapped

//////////////////////
//// Record types ////
//////////////////////

// Now, let's see what happens with records.
// Let's define and construct a value for a simple record type.
type SimpleRecord = 
    { First: SimpleInt
      Second: int }
let simpleRecord = 
    { First = SimpleInt 42
      Second = 1 }

//// Deconstruction similar to simple types

// We can deconstruct record in: 
// *lambda expression:
//  Remark: Added type annotations to r1, r2 for clarity
let (r1: SimpleInt), (r2: int) = 
    simpleRecord 
    |> fun {First = x; Second = y} -> (x, y)

// *let assignment:
let {First = r1'; Second = r2'} = simpleRecord

// *function parameter:
// Remark: We deconstruct both record and it's internal field (which is of type SimpleInt)
module SimpleRecordAdder =
    let addRecordFields {First = (SimpleInt f); Second = s} = f + s

// There is a similar parallel between construction and deconstruction of records:
let wrappedRecord = {First = SimpleInt 42; Second = 1}
let {First = (SimpleInt f); Second = s} = wrappedRecord 

//// Partial deconstruction
// If we don't need all of record fields, we can use partial deconstruction in all previously defined cases.
let {Second = s'} = wrappedRecord
let s'' = wrappedRecord |> fun {Second = x} -> x

module SecondFieldAdder =
    let addOneToSecondField {Second = s} = s + 1

let {First = (SimpleInt f')} = wrappedRecord
let f'' = wrappedRecord |> fun {First = (SimpleInt x)} -> x

module FirstFieldAdder =
    let addOneToFirstField {First = (SimpleInt f)} = f + 1

//////////////////////////
//// Pattern matching ////
//////////////////////////
   
// Pattern matching is one of the most popular usage of deconstruction.

///// Simple type

type MyInt = MyInt of int
let myVal = MyInt 42

let valueIfNotZero = 
    match myVal with 
    | MyInt 0 -> None
    | MyInt x -> Some x

//// Record type
//   We can match on all record fields:
type MyRecord = { First: int; Second: int }
let myRec = {First = 1; Second = 2}

// Full
let addIfNotZeros =
    match myRec with
    | {First = 0; Second = 0} -> None
    | {First = x; Second = y} -> Some (x + y)

// Or we can use partial deconstruction
let getSecondIfNotZero =
    match myRec with
    | {Second = 0} -> None
    | {Second = x} -> Some x

//// List
//   Lists offer a refined ways of deconstruction. 
//   Let's define a simple list and see how we can match on it.
let myList = [1;2;3;4;5]

// *Exact match
let is12345 =
    match myList with
    | [1;2;3;4;5] -> true
    | _ -> false

// *Match exact number of items
let sum5OrZero =
    match myList with
    | [a;b;c;d;e] -> a+b+c+d+e
    | _ -> 0

// *Match head/tail
//  We can match head/tail with `h::t` construct.
//  Head is the first element of the list, Tail is a list of all further elements.
//  Example use cases:
// Exact head 
let is1First =
    match myList with
    | 1::t -> true
    | _ -> false

// Exact tail
let is2345Tail =
    match myList with
    | h::[2;3;4;5] -> true
    | _ -> false

// Deconstruct head
let head =
    match myList with
    | h::t -> Some h
    | _ -> None

// Deconstruct tail
let skipHead =
    match myList with
    | h::t -> t
    | _ -> []

// Empty or not?
let isEmpty =
    match myList with
    | [] -> true 
    | _ -> false

// One element
let isSingle =
    match myList with
    | [x] -> true
    | _ -> false

//// Arrays
//   An Array structure offers fewer ways of matching than list.
//   We construct an array as:
let myArray = [|1;2;3;4;5|]

// Cases similar to List type 
// *Exact match
let is12345' =
    match myArray with
    | [|1;2;3;4;5|] -> true
    | _ -> false

// *Deconstruct exact number of items
let sum5OrZero' =
    match myArray with
    | [|a;b;c;d;e|] -> a+b+c+d+e
    | _ -> 0

// *Empty?
let isEmpty' =
    match myArray with
    | [||] -> true
    | _ -> false

// *One element
let isSingle' =
    match myArray with
    | [|x|] -> true
    | _ -> false