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.

Part 1 Table of Contents Part 3

Zurück
Zurück

Introduction to Functional Programming in F# – Part 3

Weiter
Weiter

Introduction to Functional Programming in F#