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.