Dive C1.03.3
Partial application
Supply some arguments now; a new, specialised function comes back for the rest.
Currying made every F# function a chain of one-argument steps. Partial application
stops part-way: supply some arguments and keep the function that the chain returns. From the curried
add, the call add 10 is a complete value — a function of type
(int -> int) that adds ten. This is the move that makes
List.map (add 10) read cleanly, and it pairs with the pipe and F#'s
data-last convention.
Some arguments now, a function back
Apply fewer arguments than a function has parameters, and the result is a function expecting the rest.
From add, the call add 10 binds the first argument and
leaves the second open — a new function that adds ten.
let add x y = x + y let add10 = add 10 // val add10 : (int -> int) add10 5 // 15 let multiply x y = x * y let double = multiply 2 // val double : (int -> int) List.map double [1; 2; 3] // [2; 4; 6]
Each specialised function is an ordinary value with the type the F# Interactive prints in parentheses,
(int -> int). double is multiply 2:
give it any int and it doubles it. Passed to List.map, it
runs across the whole list.
- partial application
- supply some of a function's arguments; the rest become a new function.
- add 10
- type
(int -> int)— a function that adds ten. - multiply 2
- type
(int -> int)— a function that doubles. - List.map (add 10)
- maps the specialised function across a list.
Data-last and the pipe
FSharp.Core puts the collection last in its functions: List.map
takes the function first and the list second. That ordering exists so partial application leaves exactly
the right hole. List.map (add 10) is a partially applied
List.map — a function still waiting for its list — and the pipe
|> supplies it.
let add x y = x + y [1; 2; 3] |> List.map (add 10) // [11; 12; 13] // the same, named in two steps let addTen = List.map (add 10) // val addTen : (int list -> int list) addTen [1; 2; 3] // [11; 12; 13]
The expression x |> f means f x: it feeds
x in as the last argument. Because the list is the last argument of
List.map, the pipe and partial application fit together — the function is built
with add 10, and the data arrives through the pipe.
Data-last is a convention, not a language rule, but FSharp.Core follows it across
List, Array, and Seq. It is why
partially applied collection functions slot onto the end of a pipe.
A partial-application builder
Pick a base function and a first argument. The builder forms the specialised function, then maps it across
the fixed list [1; 2; 3; 4; 5]. The readout shows the F# line and the list it
evaluates to.
base fn + first arg → specialised fn → List.map over [1; 2; 3; 4; 5]
base function
first argument
A general function plus a fixed argument is a specialised one. Partial application names the hole the data fills, and the pipe fills it.
Partial application
Fix some of a curried function's arguments; the rest become a new, named-or-passed function.
F# today
[1; 2; 3] |> List.map (add 10) builds the function with add 10 and pipes the list in.
The three together
Three facts, one shape. A function is a value, so it can be passed and returned. Currying makes every function a chain of one-argument steps. Partial application stops the chain part-way and keeps the function that comes back. Together they explain why F# code reads as pipelines of small, specialised functions rather than loops over arguments.
References
Sources
- Microsoft — F# documentation — partial application, the pipe operator, and the data-last convention.
- FSharp.Core API reference —
List.mapand the data-last collection functions. - The F# Language Specification — function application and partial application.
Related in this course
- C1.03.2 — Currying — the mechanism partial application uses.
- C1.03 — Functions — the module hub and its three dives.
- Elixir — Currying with closures — the same specialisation, built by hand on the BEAM.