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

Introduction to Functional Programming in F#

Blog 12/22/22

Introduction to Functional Programming in F# – Part 7

Explore LINQ and query expressions in F#. Simplify data manipulation and enhance your functional programming skills with this guide.

Blog 9/15/22

Introduction to Functional Programming in F# – Part 3

Dive into F# data structures and pattern matching. Simplify code and enhance functionality with these powerful features.

Blog 3/22/23

Introduction to Functional Programming in F# – Part 8

Discover Units of Measure and Type Providers in F#. Enhance data management and type safety in your applications with these powerful tools.

Blog 8/8/23

Introduction to Functional Programming in F# – Part 12

Explore reflection and meta-programming in F#. Learn how to dynamically manipulate code and enhance flexibility with advanced techniques.

Blog 3/22/23

Introduction to Functional Programming in F# – Part 9

Explore Active Patterns and Computation Expressions in F#. Enhance code clarity and functionality with these advanced techniques.

Blog 12/22/22

Introduction to Functional Programming in F# – Part 6

Learn error handling in F# with option types. Improve code reliability using F#'s powerful error-handling techniques.

Blog 7/12/23

Introduction to Functional Programming in F# – Part 11

Learn type inference and generic functions in F#. Boost efficiency and flexibility in your code with these essential programming concepts.

Blog 5/18/22

Introduction to Functional Programming in F#

Dive into functional programming with F# in our introductory series. Learn how to solve real business problems using F#'s functional programming features. This first part covers setting up your environment, basic F# syntax, and implementing a simple use case. Perfect for developers looking to enhance their skills in functional programming.

Blog 10/1/22

Introduction to Functional Programming in F# – Part 4

Unlock F# collections and pipelines. Manage data efficiently and streamline your functional programming workflow with these powerful tools.

Blog 10/11/22

Introduction to Functional Programming in F# – Part 5

Master F# asynchronous workflows and parallelism. Enhance application performance with advanced functional programming techniques.

Blog 5/17/23

Introduction to Functional Programming in F# – Part 10

Discover Agents and Mailboxes in F#. Build responsive applications using these powerful concurrency tools in functional programming.

Blog 8/7/20

Understanding F# Type Aliases

In this post, we discuss the difference between F# types and aliases that from a glance may appear to be the same thing.

Blog 7/21/20

Understanding F# applicatives and custom operators

In this post, Jonathan Channon, a newcomer to F#, discusses how he learnt about a slightly more advanced functional concept — Applicatives.

Blog 3/10/21

Introduction to Web Programming in F# with Giraffe – Part 1

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 3/12/21

Introduction to Web Programming in F# with Giraffe – Part 3

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 3/11/21

Introduction to Web Programming in F# with Giraffe – Part 2

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 11/30/22

Introduction to Partial Function Application in F#

Partial Function Application is one of the core functional programming concepts that everyone should understand as it is widely used in most F# codebases.In this post I will introduce you to the grace and power of partial application. We will start with tupled arguments that most devs will recognise and then move onto curried arguments that allow us to use partial application.

Blog 12/3/21

Using Discriminated Union Labelled Fields

A few weeks ago, I re-discovered labelled fields in discriminated unions. Despite the fact that they look like tuples, they are not.

Blog 11/30/22

Part 2: Detecting Truck Parking Lots on Satellite Images

In the previous blog post, we created an already pretty powerful image segmentation model in order to detect the shape of truck parking lots on satellite images. However, we will now try to run the code on new hardware and get even better as well as more robust results.

Blog 11/27/23

Part 4: Save Time and Analyze the Database File

ChatGPT-4 enables you to analyze database contents with just two simple steps (copy and paste), facilitating well-informed decision-making.

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!