Basics 13 - Containers, Option and Result

Time to discuss Option and Result container types.

Learning-FSharp/Ch13-ContainerOptionResult/Program.fs

Check out all the comments that are placed in the source file.

Containers and Abstraction

Containers (not an official name) or container types (again, not an official name) are the pillars of abstraction in F#. Understanding abstraction isn't the simplest of problem statements. So, let's take an example, let's first try to establish what abstraction is.

Imagine you work at the port. A ship arrives with a container full of apples, and your boss sends you a list of tasks:

  1. Report: the number and total weight of all the apples

  2. Move all the apples from the source container to a bigger target container

  3. Make sure to throw out all the bad apples during movement

  4. Report: the number and total weight of all the apples in the target container

Here's how an imperative programmer is going to do this.

An Imperative Programmer's Way

  1. Get all the tools required to open/close the containers

  2. Get the pen, paper, calculator, weighing machine etc. for counting the number and weight of all the apples

  3. Open the source and target containers

  4. Pick an apple from the source container

    1. Add to count (source side)

    2. Measure weight and add to the total weight (source side)

    3. If the apple is not bad

      1. Add to count (target side)

      2. Add to the total weight (target side)

      3. Put in the target container

  5. Keep doing step 4 for all the apples in the source container

  6. Close the source and target containers

Simple, isn't it?

Problems, Problems and Problems

All you are asked for: get the count and weight, throw out the bad ones, and move apples from one container to another. However, you can't do it unless you know of:

  1. Opening the source container which is full of apples; imagine opening a floodgate

  2. Closing/sealing the target container

  3. How apples are placed in the container so that you pick those up one by one

  4. How to place apples in the target container

Abstraction - The Helping Hand

Instead of learning, practicing and becoming an expert in dealing with containers and apples, you may decide to get someone who knows all of this. You may simplify your job. You may focus on "what you need" vs. "what you need and how to get it done".

Abstraction is:

  1. When you focus on what you need

    1. Count and Weight

    2. Movement from source to target container

    3. Define the criteria for selecting the bad apples so that those are thrown out

  2. When you have no structural awareness

    1. How to open the container

    2. How to close the container

    3. How apples are placed in the source container, or how to pick those up one by one

    4. How to place apples in the target container

Container Types + Functions

Here's a complete example based on the above problem statement. Instead of apples, we are dealing with numbers. Instead of adding weight, we are adding number values, and filtering out odd numbers.

Don't be bothered if you don't understand this example at this moment. The emphasis at the moment is on the fact that there is a container type (int list) available and a toolbox full of functions (sum and filter). All I have to do is create a pipeline and inject functions (or lambda expressions) to control "what I need".

let inputList = [ 1; 2; 3; 4; 5; 6; 7; 8; 9; 10 ]

let status =
    {| SourceList = inputList
        SourceCount = 0
        SourceWeight = 0
        TargetList = list<int>.Empty
        TargetCount = 0
        TargetWeight = 0 |}

let afterWork =
    status
    |> (fun st ->
        {| st with
            SourceCount = st.SourceList.Length |})
    |> (fun st ->
        {| st with
            SourceWeight = List.sum st.SourceList |})
    |> (fun st ->
        {| st with
            TargetList = List.filter (fun x -> x % 2 = 0) st.SourceList |})
    |> (fun st ->
        {| st with
            TargetCount = st.TargetList.Length |})
    |> (fun st ->
        {| st with
            TargetWeight = List.sum st.TargetList |})

printf
    "Job Complete. Source Count: %d, Source Total: %d, Target Count: %d, Target Total: %d"
    afterWork.SourceCount
    afterWork.SourceWeight
    afterWork.TargetCount
    afterWork.TargetWeight

There are many container types in F#, and each type has many-many functions. We will slowly discover various types as we move on. Time to discuss Option and Result.

The Option Container Type

The option type in F# is used when the presence of data is optional.

The option type is your sensible replacement for null and magic values. It's a generic discriminated union, which has 2 cases:

  1. Some: of type 't, used for cases when data is available

  2. None: used for cases when data isn't available

// Yes, there is data, 1
// int option
let a = Some 1

// Yes, there is data, "1"
// string option
let b = Some "1"

// Nope, no data
let c = None

// Person record type
// Age is of type int option
// Indicating that age may or may not be available
type Person = { Name: string; Age: int option }

// When age is known
let p1 = { Name = "Whatever"; Age = Some 1 }

// When age is not known
let p2 = { Name = "Whatever"; Age = None }

The Result Container Type

The Result type in F# is used to represent duality; either the successful result of an operation or the failure details.

It's a generic discriminated union, which has 2 cases:

  1. Ok: of type 'T, used for the successful result of an operation

  2. Error: of type 'TError, used for failure details

// Indicates success, result = 1
let x = Ok 1

// Indicates success, result = "1"
let y = Ok "1"

// Indicates failure
let z = Error (System.ArgumentException("Don't like your argument!"))

Pattern Matching for Option and Result

When using option:

match opt with
| Some(x) -> $"Found {x}."
| None -> "Found Nothing."

When using Result:

match res with
| Ok(x) -> $"Success with {x}."
| Error(x) -> $"Failure with {x}"

Functions for Option and Result Containers

At this point, I am not covering functions for option and Result. That's because I don't want to overwhelm you with too much information.

There are a few more concepts that you need to understand before functions for these types are discussed. So, be patient.

If you have reached so far, congratulations.

Keep reading!