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:
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:
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.
Part 1 Table of Contents Part 3
Introduction to Functional Programming in F#