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.

Tupled Arguments

Let's start with a simple function called add that takes a tuple of ints and returns an int as the result:

// int * int -> int
let add (a:int, b:int) : int = a + b

Throughout this post, we will be concentrating on the function type signatures, in this case int * int -> int. Whenever you see an arrow -> in a type signature, that implies a function. The name (binding) add is a function that takes a tuple (defined by the *) of int and int as input, and returns an int as the result.

The F# compiler is able to infer most types through usage, so we can rewrite our function without the types if we wish:

// int * int -> int
let add (a, b) = a + b

To use the add function, we can do the following:

// int
let result = add (2, 3) // returns 5

You can also lose the space after the binding name if you wish:

// int
let result = add(2, 3) // returns 5

Tupled arguments are a useful tool but they tend to be used less than the other form of function arguments, curried, because they don't support partial application.

Curried Arguments

This is the same function written using curried arguments:

// int -> int -> int
let add a b = a + b

No commas or brackets but more importantly, a change in the function signature to int -> int -> int. I will cover the meaning of the multiple arrows later in the post but for now, let's continue with how to use this function:

// int
let result = add 2 3 // returns 5

No real difference is there? The fun happens when we ask 'What happens if I only supply the first argument?':

// int -> int
let result = add 2

It doesn't error! Instead, if we look at the signature, result is a function that takes an int and returns an int! If I then supply the second argument, I will get the actual value from the function.

// int
let result2 = result 3 // returns 5

This is Partial Function Application. The ability to use a subset of the arguments and to get a result only when all of the other required arguments are supplied. Let's have a look at another example:

// string -> int -> string
let example (a:string) (b:int) =
    String.replicate b a 

// string
let runExample = example "Hello" 2 // returns "HelloHello"

We have a function that takes a string and an int and returns a string.

What happens if I supply a subset of the arguments out of order?

let runExample = example 2 // Error - you can't do this

It doesn't work. You must supply the argumants in order. Ordering of arguments, particularly the last one, but also as we will see later, the first one(s) matters.

Let's try partially applying the arguments and we should get a function that takes an int and returns a string:

// int -> string
let runExampleHello = example "Hello"

I've gone from string -> int -> string to int -> string after supplying the first string argument. If I now supply the number of repetitions argument, it will return a string result because I have supplied all of the required arguments:

// string
let result = runExampleHello 5 // returns "HelloHelloHelloHelloHello"

If you want to, you can make the input parameter explicit so that you can change it easily:

// int -> string
let runExample i = example "Hello" i

let result = runExample 2 // returns "HelloHello"

I could also rewrite the function to use the forward pipe operator:

// int -> string
let runExample i = i |> example "Hello"

The implication of this is that forward piping uses partial application.

Passing Functions as Parameters

Functions are first class citizens in F#. This means that we can do interesting things with them like pass them as arguments into other functions:

// (int -> int -> int) -> int -> int -> int
let calculate (f:int -> int -> int) a b = f a b

Our type signature shows that we pass in a function that takes two ints and returns an int plus two other ints and returns an int.

Let's call the calculate function with the add function we created earlier as it has a type signature that matches f:int -> int -> int:

// int
let result = calculate add 2 3 // returns 5

Let's create a new function that matches the signature that multiplies instead:

// int -> int -> int
let multiply a b = a * b

We use in the same way:

// int
let result = calculate multiply 2 3 // returns 6

If we decide not to pass in the last argument, we get:

// int -> int
let result = calculate multiply 2

Adding the required last argument give us:

// int
let result2 = result 3 // returns 6

I can pass in an anonymous function if I like:

// int
let result = calculate (fun a b -> a + b) 2 3

What do you think the function signature of the following multiply function is?

// ?
let multiply = calculate (fun a b -> a * b)

Hopefully, you will agree that it is int -> int -> int.

Realish example

This example will use the function injection ideas to allow us to test some code that has side effects.

We'll create a record type of Customer:

type Customer = {
    Id : int
    Name: string
}

Then create a function that simulates a database call to get a Customer:

// string -> int -> Customer
let getCustomerFromDb (connString:string) (id:int) =
    // Imagine we are getting data from a Db. 
    // We are not handling missing data [Option or Result]
    { Id = 1; Name = "Real Customer" }

Finally, we create a function that uses the db function:

// string -> int -> Customer
let doStuff (connString:string) (id:int) = 
    let customer = getCustomerFromDb "my_conn_string" id
    customer

How do we test this without using a database? There are a few options but we are going to use partial application. The first thing we will do is create a helper function that sits between the previous two functions that we will inject a function into:

// (int -> Customer) -> int -> Customer
let getCustomer (getCustomerService:int -> Customer) (id:int) =
    getCustomerService id

We then modify the main caller function so that we can pass in a partially applied database function by only providing the first string argument:

// string -> int -> Customer
let doStuff (connString:string) (id:int) = 
    // int -> Customer
    let customerService = getCustomerFromDb "my_conn_string"
    let customer = getCustomer customerService id
    customer

We could rewrite it to look like this:

// string -> int -> Customer
let doStuff (connString:string) (id:int) = 
    let customer = getCustomer (getCustomerFromDb "my_conn_string") id
    customer

We would then add a test module and include the following helper function that we will use instead of the database calling function:

// int -> Customer
let getCustomerStub (id:int) =
    { Id = 1; Name = "Test Customer" }

Finally, we write a test that calls the main doStuff function:

// unit -> bool
let ``should return a valid customer`` () =
    let expected = { Id = 1; Name = "Test Customer" }
    let customer = getCustomer getCustomerStub expected.Id
    customer = expected

Partial application is a nice way to help keep your functions with side effects away from your core business logic functions.

Finally, why do we have multiple arrows in the add function?

Under the covers

We have our add function that takes two arguments (int and int) and returns an int:

// int -> int -> int
let add a b = a + b

Actually, that is a lie. Functions in F# take one argument as input and return one item as output. So what's going on? Firstly we will rewrite or function in a slightly different style:

// int -> int -> int
let add a = fun b -> a + b

It has the same signature as before but we could read the code as 'the function add takes an int 'a' as input and returns a function that takes int 'b' as input and returns an int result'. It might help to write the signature as int -> (int -> int). If we extend it to three inputs, we get:

// int -> (int -> (int -> int))
let add a =
    fun b ->
        fun c -> a + b + c

In this case, add is a function that takes an int a as input and returns a function that takes an int b as input and returns a function that takes an int c as input and returns an int as output.

In practical terms, knowing what goes on under the covers doesn't actually have much impact as you can treat a function with multiple input arguments as just that but it's important to know that not providing all of the argumants results in a function being returned with the remaining unsatisfied arguments instead of a value.

Further Example

If you look at my Functional Validation in F# Using Applicatives post for last year's calendar, you'll see that that makes use of the techniques that we have used today for the happy path.

Summary

Using curried arguments opens the door to partial function application in F#. It is one of the most powerful and useful techniques we have for functional programming in F#.

Understanding what your function signatures are telling you is important, as is trusting the compiler.

Thanks to Sergey Tihon for F# Weekly and for running the F# Advent Calendar, Scott Wlaschin for writing https://pragprog.com/book/swdddf/domain-modeling-made-functional and making https://fsharpforfunandprofit.com/ such an amazing resource plus special thanks to all of you in the F# community for being so awesome. :)

https://twitter.com/ijrussell

Zurück
Zurück

Using GCP Cloud Functions with F#

Weiter
Weiter

Innovation Incubator at Trustbit