Currying and Partial Application: Functions Returning Functions
In previous chapters, we’ve seen that functions are first-class values with types, and even operators like (+)
are essentially functions.
We also previewed how applying only one argument to (+)
(e.g., (+) 1
) created a new function (add1
).
This behavior, where providing an argument to a function that expects multiple arguments results in a new function, is a direct consequence of HOF Pattern 1 ('a -> ('b -> 'c)
) discussed in Section 3.
Let’s now explore the underlying mechanism that makes this possible: Currying, and its practical outcome, Partial Application.
Revisiting Multi-Argument Functions and Type Signatures
Section titled “Revisiting Multi-Argument Functions and Type Signatures”Consider a function for multiplication. In many languages, you might define it to accept two arguments directly:
function multiply(x, y) { return x * y; }// Expects x and y together
In F#, a similar definition let multiply x y = x * y
appears to also take two arguments. However, its type signature, typically int -> int -> int
, tells a deeper story.
As discussed in Section 3 regarding type signatures, this is shorthand for int -> (int -> int)
. This nested structure implies that the function fundamentally operates by taking arguments one at a time.
Currying: The “One Argument at a Time” Mechanism
Section titled “Currying: The “One Argument at a Time” Mechanism”This “one argument at a time” behavior is achieved through a process called Currying. Many functional languages, including F#, automatically transform functions that appear to take multiple arguments (like let multiply x y = ...
) into a sequence of nested functions, each accepting a single argument.
The definition let multiply x y = x * y
is essentially convenient syntax for:
let multiply = fun x -> (fun y -> x * y)// Type: int -> (int -> int)// or int -> int -> int
This multiply
function now works as follows:
- It takes the first argument
x
(anint
). - It returns a new function (
fun y -> x * y
). This new function has “remembered”x
and has the typeint -> int
. This step perfectly aligns with HOF Pattern 1 ('a -> ('b -> 'c)
), where'a
is the type ofx
, and'b -> 'c
is the type of the returned function (int -> int
). - This new function then takes the second argument
y
(anint
). - Finally, it performs the calculation
x * y
and returns theint
result.
This transformation, where a function taking multiple arguments is expressed as a chain of functions each taking a single argument and returning the next function in the chain (until the final value is computed), is known as Currying, named after the mathematician Haskell Curry.
Partial Application: The Natural Result of Currying
Section titled “Partial Application: The Natural Result of Currying”With currying in place, Partial Application becomes a natural consequence.
- Definition: Partial application is simply the act of calling a function with fewer arguments than it notionally expects.
- In a Curried System: Since all functions fundamentally take one argument at a time, applying the first argument(s) to a curried function is partial application. The result is the intermediate function that’s next in the curried chain. No special syntax is needed.
So, when we wrote let double = (*) 2
in the previous chapter:
let multiplyOperatorAsFunction = (*)// Type: int -> int -> int// or int -> (int -> int)let double = multiplyOperatorAsFunction 2// Apply first arg '2'// 'double' is now// 'int -> int'let result = 10 |> double// result is 20
multiplyOperatorAsFunction 2
(or (*) 2
) is a partial application. The (*)
function (type int -> (int -> int)
) receives its first int
argument (2
) and returns the intermediate function (type int -> int
), which we named double
.
Analogy: The Multiplication Table
Section titled “Analogy: The Multiplication Table”Let’s visualize this using the familiar multiplication table.
1. The Full Operation: The complete multiplication operation, represented by the binary function (*)
, needs two numbers (e.g., a row number and a column number) to give you a result from the table. It corresponds to the entire table:
(Requires two inputs, like (*) 3 4
)
2. Fixing One Argument (Partial Application): Now, what happens if we partially apply the multiplication function by fixing the first number, say, to 3?
In F#, we write this as (*) 3
. This is like selecting just one row from the table – the “3 times” row:
(Represents the function (*) 3
)
By providing only the first argument (3
) to the two-argument function (*)
, we’ve created a new function.
Let’s call it multiplyBy3
.
This new function only needs one more argument (the number for the column) and corresponds to this specific row.
// Create a new function "multiplyBy3"// by partially applying (*) with 3let multiplyBy3 = (*) 3
multiplyBy3
is the “3 times table” function; it waits for one more number.
3. Applying the New Function:
Once we have our specialized function multiplyBy3
, we can give it the final argument.
For example, applying it to 4
(multiplyBy3 4
) is like looking up the 4th column in the 3rd row to find the result 12
:
(Represents applying the function: multiplyBy3 4
)
// Now use the new function - it only needs one argumentlet result = multiplyBy3 4 // result is 12 (3 * 4)printfn "3 times 4 is: %d" result
Applying this to our previous examples:
This process of fixing one argument to create a new, simpler function is exactly what we did earlier:
// Create a 'multiply by 2' function from (*)let double = (*) 2
// Create an 'add 1' function from (+)let add1 = (+) 1
We created specialized unary functions (double
, add1
) from general binary functions ((*)
, (+)
) using partial application.
This ability to easily create new, specialized functions from existing ones by partially applying arguments is a common and powerful technique in FP.
Connecting Partial Application to HOF Pattern 1
Section titled “Connecting Partial Application to HOF Pattern 1”As demonstrated with the multiplyBy3
, double
, and add1
examples, partial application takes some initial input (like the number 3
for (*)
, or 2
for (*)
, or 1
for (+)
) and returns a new function.
This perfectly matches HOF Pattern 1:
Value |> Function = Function
// Create a 'multiply by 2' function from (*)let double = 2 |> (*)
// Create an 'add 1' function from (+)let add1 = 1 |> (+)
Value |> Function = Value
let result = 5 |> double // 10
let result' = 10 |> add1 // 11
Therefore, partial application is a prime example of Higher-Order Functions in action, specifically illustrating the pattern where functions return other functions. It showcases how treating functions as first-class values allows us to manipulate and create new functions dynamically.
Summary
Section titled “Summary”- Many functional languages employ Currying, a mechanism where functions appearing to take multiple arguments are automatically treated as a sequence of functions each taking a single argument and returning the next function in the chain, until the final result is produced.
- A type signature like
T1 -> T2 -> TResult
reflects this, being shorthand forT1 -> (T2 -> TResult)
. This is an instance of HOF Pattern 1. - Partial Application is the natural outcome of applying arguments to a curried function. Providing fewer arguments than notionally specified results in an intermediate function being returned.
- This mechanism allows for the easy creation of specialized functions (like
add1
ordouble
) from more general ones (like the operators(+)
or(*)
). - This entire behavior—a function taking an argument and returning a new function—is a prime example of HOF Pattern 1 in action.
It might seem that F#‘s unary function model makes it awkward to pass multiple related pieces of data (like coordinates (x, y)
) compared to JavaScript’s multi-argument functions (f(x, y)
).
While currying handles functions that logically take multiple independent arguments step-by-step, what if you simply want to pass a single, grouped piece of data containing multiple components?
F# addresses this with Tuples. A tuple, written (a, b)
or (a, b, c, ...)
, groups multiple values into a single, composite value. This is different from a list ([a; b; c]
) and is a data structure not present in the same way in JavaScript.
Because a tuple like (x, y)
is considered a single value, it can be passed as the one argument to a unary F# function:
// Define a function that takes ONE argument: a tuple of two integerslet addCoordinates (coords: int * int) = let (x, y) = coords // Deconstruct the tuple inside the function x + y
// Call the unary function,// passing the tuple as the single argumentlet result = addCoordinates (3, 4)// result is 7
Notice that the function call addCoordinates (3, 4)
looks syntactically similar to a JavaScript call addCoordinates(3, 4)
which might take two separate arguments.
However, in F#, addCoordinates
is still a unary function accepting a single tuple value.
This provides a convenient syntax for working with grouped data within the unary function model, offering another example of F#‘s pragmatic and expressive design.