Introduction to Functional Programming in F# - Part 3

Introduction

So far in this series we have covered a lot of the fundamental functional programming concepts. In this post we will investigate null handling and exceptions.

Null Handling

Most of the time, you will not have to deal with null in your F# code as it has a built-in type called Option that you will use instead. It looks very similar to this:

type Option<'T> =
    | Some of 'T
    | None

It is a discriminated union with two choices. The ' is the F# way of showing the type is a generic. Any type can be made optional.

Create a new file in your folder called option.fsx. Add 'open System' without the quote marks to the top of the file.

Don't forget to highlight and run the code examples in this post in F# Interactive (FSI).

We'll create a function to try to parse a string as a DateTime.

let tryParseDateTime (input:string) =
    let (success, result) = DateTime.TryParse input
    if success then Some result
    else None

You can also pattern match directly:

let tryParseDateTime (input:string) =
    match DateTime.TryParse input with
    | true, result -> Some result
    | _ -> None

I prefer the previous version because it reads more easily to my eyes.

Run the following examples:

let isDate = tryParseDateTime "2019-08-01"
let isNotDate = tryParseDateTime "Hello"

You will see that the valid date returns Some of the valid date and the non-date string will return None.

Another way that the Option type can be used is for optional data like a person's middle name as not everyone has one:

type PersonName = {
    FirstName : string
    MiddleName : string option // or Option<string>
    LastName : string
}

If the person doesn't have a middle name, you set it to None and if they do you set it to Some "name".

let person = { FirstName = "Ian"; MiddleName = None; LastName = "Russell"}
let person' = { person with MiddleName = Some "????" }

Notice that we have used the copy-and-update record expression we met in the last post.

Sadly, there is one area where nulls can sneak into your codebase; through interop with code/libraries using other .Net languages.

Interop With .NET

If you are interacting with code written in C#, there is a chance that you will have some null issues. In addition to the Option type, F# also offers the Option module that contains some very useful helper functions to make life easier.

Let's create a null for both a .Net Reference type and a .Net Nullable primitive:

let nullObj:string = null
let nullPri = Nullable<int>()

Run the code in FSI to prove that they are both null.

To convert from .Net to an F# Option type, we can use the Option.ofObj and Option.ofNullable functions:

let fromNullObj = Option.ofObj nullObj
let fromNullPri = Option.ofNullable nullPri

To convert from an Option type to .Net types, we can use the Option.toObj and Option.toNullable functions.

let toNullObj = Option.toObj fromNullObj
let toNullPri = Option.toNullable fromNullPri

Run the code in FSI to show that this works correctly:

What happens if you want to convert from an Option type to something that doesn't support null but instead expects a placeholder value? You could use pattern matching as Option is a discriminated union or you can use another function in the Option module:

let result = Option.defaultValue "------" fromNullObj

If the Option value is Some, then the value inside the Some is returned otherwise the default is returned.

If you use this a lot, you may find that using Partial Application might make the task more pleasurable by reducing the amount of code you may need to write. We create a function that takes the default but not the Option value:

let unknown = Option.defaultValue "????" // (string option -> string)

let result = unknown fromNullObj

As you can see, handling of null and optional values is handled very nicely in F#. You should never see a NullReferenceException. :)

Handling Exceptions

Create a new file called result.fsx in your folder.

WE will create a function that does simple division but returns an exception if the divisor is 0:

open System

let tryDivide (x:decimal) (y:decimal) = // decimal -> decimal -> decimal
    try
        x/y 
    with
    | :? DivideByZeroException as ex -> raise ex

Whilst this code is perfectly valid, the function signature is lying to you; It doesn't always return a decimal. The only way I would know this is by looking at the code or getting the error when the code executed. This goes against the general ethos of F# coding.

Most functional languages implement a type that offers a choice between success and failure; F# is no exception. This is an example of a potential implementation:

type Result<'TSuccess,'TFailure> = 
   | Success of 'TSuccess
   | Failure of 'TFailure

Unsurprisingly, there is one built into the language (from F# 4.1) but rather than Success/Failure, it uses Ok/Error. Let's use the Result type in our tryDivide function:

let tryDivide (x:decimal) (y:decimal) = // decimal -> decimal -> Result<decimal,exn>
    try
        Ok (x/y) 
    with
    | :? DivideByZeroException as ex -> Error ex

let badDivide = tryDivide 1M 0M
let goodDivide = tryDivide 1M 1M

The failure type, exn, is the built-in F# error type. We'll use custom error types in a later post. Next we are going to look at how we can incorporate the Result type into the composition code we used in the last post.

Function Composition With Result

I have modified the getPurchases and increaseCreditIfVip functions to return Result types but have left the tryPromoteToVip function alone.

type Customer = {
    Id : int
    IsVip : bool
    Credit : decimal
}

let getPurchases customer = // Customer -> Result<(Customer * decimal),exn>
    try
        // Imagine this function is fetching data from a Database
        let purchases = if customer.Id % 2 = 0 then (customer, 120M) else (customer, 80M)
        Ok purchases
    with
    | ex -> Error ex

let tryPromoteToVip purchases = // Customer * decimal -> Customer
    let customer, amount = purchases
    if amount > 100M then { customer with IsVip = true }
    else customer

let increaseCreditIfVip customer = // Customer -> Result<Customer,exn>
    try
        // Imagine this function could cause an exception            
        let result = 
            if customer.IsVip then { customer with Credit = customer.Credit + 100M }
            else { customer with Credit = customer.Credit + 50M }
        Ok result
    with
    | ex -> Error ex

let upgradeCustomer customer =
    customer 
    |> getPurchases 
    |> tryPromoteToVip // Problem
    |> increaseCreditIfVip

let customerVIP = { Id = 1; IsVip = true; Credit = 0.0M }
let customerSTD = { Id = 2; IsVip = false; Credit = 100.0M }

let assertVIP = upgradeCustomer customerVIP = Ok {Id = 1; IsVip = true; Credit = 100.0M }
let assertSTDtoVIP = upgradeCustomer customerSTD = Ok {Id = 2; IsVip = true; Credit = 200.0M }
let assertSTD = upgradeCustomer { customerSTD with Id = 3; Credit = 50.0M } = Ok {Id = 3; IsVip = false; Credit = 100.0M }

Notice that there is a problem in the upgradeCustomer function on the call to tryPromoteToVip because the function signatures don't match up any longer.

Scott Wlaschin (https://fsharpforfunandprofit.com/rop/) visualises composition with the Result type as two parallel railway tracks which he calls Railway Oriented Programming (ROP), with one track for Ok and one for Error. He defines tryPromoteToVip as a one track function because it doesn't output a Result type and executes on the Ok track.

To fix the problem, we need to create a function that converts a normal one track function function into a Result function. The reason it is called map will become obvious later in this post:

let map oneTrackFunction resultInput = // ('a -> 'b) -> Result<'a,'c> -> Result<'b,'c>
    match resultInput with
    | Ok s -> Ok (oneTrackFunction s)
    | Error f -> Error f

Functions are first class citizens which means that you can use functions as inputs to other functions. With the map function, if the input is Error, the one track function never gets called and the error is passed through. Let's plug the map function in.

let upgradeCustomer customer =
    customer 
    |> getPurchases 
    |> map tryPromoteToVip 
    |> increaseCreditIfVip // Problem

That has fixed the problem with tryPromoteToVip but has pushed it on to increaseCreditIfVip. The function increaseCreditIfVip is a switch function in ROP which means that it has a Result output but doesn't handle the Error input case.

We need to create another function that converts a switch function into a Result function. It is very similar to the map function. Again the reason for calling it bind will become obvious later in this post:

let bind switchFunction resultInput = // ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c>
    match resultInput with
    | Ok s -> switchFunction s
    | Error f -> Error f

The difference between bind and map is that we don't need to wrap the output in an Ok on the Ok track for bind.

Let's plug the bind function in.

let upgradeCustomer customer =
    customer 
    |> getPurchases 
    |> map tryPromoteToVip 
    |> bind increaseCreditIfVip

The code should now have no compiler warnings. Run the asserts to verify the code works as expected.

The reason for using map and bind as function names is because the Result module has them built in as it is a common requirement. Let's update the function to use the Result module functions rather than our own.

let upgradeCustomer customer =
    customer 
    |> getPurchases 
    |> Result.map tryPromoteToVip 
    |> Result.bind increaseCreditIfVip

We can delete our map and bind functions.

If you want to learn more about this style of programming, I highly recommend Scott's book "Domain Modelling Made Functional" (https://pragprog.com/book/swdddf/domain-modeling-made-functional). Not only does it cover ROP but also lots of very useful Domain-Driven Design information.

Summary

Another post completed with a lot of really useful features and concepts covered.

  • Option type and module
  • Result type and module
  • Exception handling
  • Null handling
  • Functions as inputs to other functions

In the next post we look at testing.