Introduction to Functional Programming in F# – Part 2
Introduction
In the last post, we learned about some of the core features of functional programming in F#. In this post we are going to concentrate on functions.
A function has the following rules:
Always returns the same output for the same input
Has no side effects
Has one input and one output
Has immutable input and output
Pure functions that satisfy these rules have many benefits; They are easy to test, are cacheable and parallelizable. However, you application cannot consist only of pure functions as you probably have side effects like user input or persisting to a database.
If you read the previous post, you will remember that we wrote a function that had two parameters. I will show you why that fact and the one input rule are not conflicting. I said in the last post that function signatures are very important; In this post you will see why.
You can do a lot in a single function but you can do more and have better modularity by combining a few smaller functions together. We call this Function Composition.
Function Composition - Theory
We have two functions (f1 and f2) that look like this pseudocode:
f1 : 'a -> 'b f2 : 'b -> 'c
As the output of f1 matches the input of f2, we can combine them together to create a new function:
f3 = f1 >> f2 // 'a -> 'c
Treat the >> operator as a general purpose composition operator for two functions.
What happens if the output of f1 does not match the input of f2?:
f1 : 'a -> 'b f2 : 'c -> 'd where 'b <> 'c
To resolve this, we would create an adaptor function (or use an existing one) that we can plug in between f1 and f2:
f3 : 'b -> 'c
After plugging f3 in, we can create a new function f4:
f4 = f1 >> f3 >> f2 // 'a -> 'd
That is function composition. Let's look at a concrete example.
Function Composition - In Practice
I've taken, and slightly simplified, some of the code from Jorge Fioranelli's excellent F# Workshop: http://www.fsharpworkshop.com. Once you've finished this post, I suggest that you download the workshop (it's free!) and complete it. If you have installed an F# environment as I explained in my previous post, you have everything you need to complete it.
This example has a simple Record type and three functions that we can compose together because the function signatures match up.
type Customer = { Id : int IsVip : bool Credit : decimal } let getPurchases customer = // Customer -> (Customer * decimal) if customer.Id % 2 = 0 then (customer, 120M) else (customer, 80M) let tryPromoteToVip purchases = // (Customer * decimal) -> Customer let customer, amount = purchases if amount > 100M then { customer with IsVip = true } else customer let increaseCreditIfVip customer = // Customer -> Customer if customer.IsVip then { customer with Credit = customer.Credit + 100M } else { customer with Credit = customer.Credit + 50M }
There a couple of things in this code that we haven't seen before.
The function getPurchases returns a Tuple. Tuples are another of the types in the F# Algebraic Type System (ATS). They are an AND type like the Record type and are used for transferring data without having to define the type. Notice the difference between the definition (Customer * decimal) and the usage (customer, amount). In the tryPromoteToVip function the tuple is decomposed using Pattern Matching into it's constituent parts.
The other new feature is the copy-and-update record expression. This allows you to create a new record instance based on another, usually with some modified data.
There are four ways to compose these functions into a another function:
let upgradeCustomerComposed = // Customer -> Customer getPurchases >> tryPromoteToVip >> increaseCreditIfVip
The function upgradeCustomerComposed uses the built-in function composition operator.
let upgradeCustomerNested customer = // Customer -> Customer increaseCreditIfVip(tryPromoteToVip(getPurchases customer)) let upgradeCustomer customer = // Customer -> Customer let customerWithPurchases = getPurchases customer let promotedCustomer = tryPromoteToVip customerWithPurchases let increasedCreditCustomer = increaseCreditIfVip promotedCustomer increasedCreditCustomer let upgradeCustomerPiped customer = // Customer -> Customer customer |> getPurchases |> tryPromoteToVip |> increaseCreditIfVip
The upgradeCustomerPiped uses the forward pipe operator (|>). It is the equivalent to the upgradeCustomer function above it but without having to specify the intermediate values. The value from the line above get passed as the last input argument of the next function.
Use the composition operator if you can, otherwise use the forward pipe operator.
It is quite easy to verify the output of the upgrade functions using FSI. Try replacing the upgrade function with any of the others to confirm that they produce the the same results.
let customerVIP = { Id = 1; IsVip = true; Credit = 0.0M } let customerSTD = { Id = 2; IsVip = false; Credit = 100.0M } let assertVIP = upgradeCustomerComposed customerVIP = {Id = 1; IsVip = true; Credit = 100.0M } let assertSTDtoVIP = upgradeCustomerComposed customerSTD = {Id = 2; IsVip = true; Credit = 200.0M } let assertSTD = upgradeCustomerComposed { customerSTD with Id = 3; Credit = 50.0M } = {Id = 3; IsVip = false; Credit = 100.0M }
Record types use Structural Equality which means that if they look the same, they are equal.
Unit
All functions must have one input and one output. To solve the problem of a function that doesn't need an input or produce an output, F# has a type called unit.
let now () = System.DateTime.Now // unit -> System.DateTime let log msg = // 'a -> unit // Log message ()
Unit appears in the function signature as unit but in code you use ().
Multiple Arguments
All functions must have one input and one output but last time we created a function with multiple input arguments:
let calculateTotal customer spend = ... // Customer -> decimal -> decimal
Let's write the function signature slightly differently:
Customer -> (decimal -> decimal)
The function calculateTotal is a function that takes a Customer as input and returns a function as output that takes a decimal as input and returns a decimal as output. This is called Currying after Haskell Curry, a US Mathematician. It allows you to write functions that have multiple input arguments but also opens the way to a very powerful functional concept; Partial Application.
Let's look at a use case that will show Partial Application; Logging.
type LogLevel = | Error | Warning | Info let log (level:LogLevel) message = // LogLevel -> string -> unit printfn "[%A]: %s" level message ()
To partially apply this function, I'm going to define a new function that takes the log function and it's level argument but not the message.
let logError = log Error // string -> unit
The name logError is bound to a function that takes a string and returns unit. So now, instead of using
let m1 = log Error "Curried function"
I can use the logError function instead:
let m2 = logError "Partially Applied function"
As the return type is unit, you don't have to let bind the function:
log Error "Curried function" logError "Partially Applied function"
When you use functions that return unit in real applications, you will get warned to ignore the output. You do that like this:
logError "Error message" |> ignore
Partial Application is a very powerful concept that is only made possible because of the concept of Currying input arguments.
Summary
In this post we have covered:
Functions
Function Composition
Tuples
Copy-and-update record expression
Currying & Partial Application
We have now covered the fundamental building blocks of Functional Programming; Composition of types and functions.
In the next post we will investigate the handling of NULL and Exceptions.