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. :)