Functions and partial application
A function is a value with parameters, written without brackets and called without commas. Give it fewer arguments than it takes and the answer is another function.
Parameters are separated by spaces
A function is a let with parameters after the name: let double n = n * 2. There are no parentheses or commas around them, and a call looks the same — double 21 — which is why F# reads like a sentence. Parentheses are only for grouping a subexpression: double (addTen 1) applies addTen first, while double addTen 1 hands the function addTen to double.
Application binds tighter than any operator, so add 1 2 * 10 means (add 1 2) * 10. When a line is doing two things at once, put the parentheses in: they cost nothing and they say which part happens first.
One argument at a time
Every function takes exactly one argument. A two-parameter function is a function that takes one argument and returns a function for the next, which is why add 10 on its own is a perfectly good value: it is a function waiting for its second number. Naming it — let addTen = add 10 — is partial application, and it is how small helpers are made from general ones without writing a lambda.
The order of the parameters decides which helpers are useful. add 10 reads as "add ten to something", so the thing that varies goes last; that convention is also what makes the pipes of the next chapter work.
Saying what a parameter is
Sometimes the compiler cannot tell what a parameter should be, usually with arithmetic, because + and * work for several numeric types. An annotation settles it: let area (w: float) (h: float) = w * h. Annotations are read by people as much as by the compiler, and a function with typed parameters documents itself.
A function with no annotation can be generic. let identity x = x has the type 'a -> 'a, meaning it works for any type and the compiler settles on one at each call. That is not the same as a function that takes an obj: a generic function keeps the type it was given, so identity 3 is an int and identity "x" is a string, with no cast at either end.
Two parameters, and a partial application of them
let double n = n * 2
let add a b = a + b
let addTen = add 10
let area (w: float) (h: float) = w * hOne function, three ways to use it
Run it, then work out why `add 1 2 * 10` is not 21 before you read the output. Finally write a `subtract` function and a partial application of it.
addTen is not a special form: it is add with one of its two arguments already supplied. Any function can be treated this way, which is why F# code rarely needs a wrapper function that just forwards its arguments.
The arrow in the type
The type of add is written int -> int -> int, and the arrows are read from the right: a function taking an int and answering a function that takes an int and answers an int. Supplying one argument therefore leaves int -> int, which is exactly what add 10 is.
A tuple parameter is one parameter, not two. let addPair (a, b) = a + b has the type (int * int) -> int, and it is called with one pair in parentheses. addPair 1 2 does not compile, because the function is waiting for a single tuple and has been handed an int.
The same notation describes functions that take functions. applyTwice has the type ('a -> 'a) -> 'a -> 'a: give it a function from a value to the same kind of value and it answers a function of that kind. The compiler writes these types for you, and reading them is often faster than reading the body.
Annotate the part that is unclear
Annotations are local, not a contract for the whole file. Put one on a parameter when the compiler cannot tell which numeric type is meant, or when a reader would otherwise have to guess. Annotating everything makes the code noisier without making it safer.
An annotation does not change the value; it removes a choice the compiler would otherwise have to make. When a function is used in a way that already fixes the type, the annotation can be dropped and the code still compiles, which is a useful sign that it was documenting rather than rescuing the definition.
add(1, 2) still passes one tuple, not two arguments, even with no space after the name. double 3 + 1 doubles 3 and then adds one, rather than doubling 4, so put the sum in parentheses when that is the argument you mean. And a function called with one argument too many is a type error, not a partial application.
Try these next
- Write
let subtract a b = a - b, then makesubtractTenand check that it subtracts ten from its argument. - Give
areaa return annotation as well as parameter annotations, and confirm that the type has not changed. - Call a tuple function with two arguments, then with one pair, and compare the two compiler messages.
Small functions made from general ones
Run it and follow `stock` from `label` to its one argument. Then make a `warning` prefix, a `repeat 5` rule of a different character, and a `quadruple` built from `scale`.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
With add a b = a + b, what is add 1 2 * 10?
