Thinking in Functions
Functions Are Simple
Functions are fundamentally simple. Every function can be thought of as a mapping from one input to one output.
Functions that take multiple arguments
Let’s look at an example in JavaScript:
Imagine if it were written like this:
Now, add
takes a single input (a number) and returns a single output (a function that takes a number and returns a number).
This pattern is known as currying, named after Haskell Curry.
Curried functions support partial application, which allows us to call a function one argument at a time, returning a function until all arguments are supplied:
While this may seem counterintuitive at first, it offers a much simpler way of thinking about functions: one input, one output.
In fact, in some languages, functions are curried by default:
Let’s port the following example from TypeScript to OCaml:
Here is the equivalent in OCaml:
We can see the types are very similar. JavaScript has numbers instead of integers, and OCaml has slightly nicer syntax, but conceptually they’re equivalent.
💡 A function that takes two arguments is called a binary function, whereas a function that takes one argument is called a unary function.
We can extend this concept ad infinitum:
int -> int
: takes1
int, returns1
intint -> int -> int
: takes2
ints, returns1
intint -> int -> int -> int
: takes3
ints, returns1
int
💡 Currying is a simple but powerful concept that allows us to think about functions the same way regardless of language or syntax: one input, one output.
Functions that take no arguments
Let’s look at an example in TypeScript and OCaml respectively:
This function takes no arguments and returns a number, which appears to be zero inputs.
Conceptually, we can think of foo
as a function that takes a “unit” type ()
as input and returns a number as output. In fact, let’s compare the respective types of foo
:
💡 A function that takes no arguments is called a nullary function
Functions that don’t return a value
Functions that don’t return a value are typically used to perform side effects, such as logging to the console or writing to a file.
In functional programming, we say these functions return unit, which is how we express side effects with no return value.
Let’s look at an example in both TypeScript and OCaml:
💡 TypeScript uses the type
void
to express side effects with no return value, whereas OCaml uses the typeunit
.
Functions Are Composable
Functions can be chained together using composition, where the output of one function becomes the input of another. Many functional languages provide pipe operators to make this composition more readable.
Let’s look at an example in OCaml that transforms a list of numbers:
Using OCaml’s |>
pipe operator, we can make this more readable by chaining the operations into a pipeline:
Let’s inline some of the expressions:
Equivalent program in JavaScript:
💡 The pipe operator
|>
in OCaml takes a value on the left and passes it as the last argument to the function on the right.
Functions Are Everywhere
Conceptually, we can think of +
as a function that takes two numbers as input and returns a number as output.
In other words, if we could get a type for +
, it would look something like number -> number -> number
.
In fact, in some languages, operators are literally just functions! For example, in OCaml, we can use +
as a function by wrapping it in parentheses:
Another way to think about it:
💡
1 + 2
is called infix notation, whereas(+) 1 2
is known as prefix notation or Polish notation
Same with multiplication:
Let’s apply what we’ve learned so far.
We’ll write an OCaml program that maps over a list of integers and adds 3 to each element:
💡 Note: don’t worry about understanding the syntax of
in
keyword — for now just pretend it’s like a semicolon in JavaScript.
Recall that add
is a curried function, so we can use it like this:
Also recall that +
is a function, so we can use it like this:
Now add
is basically just an alias for (+)
, so we can inline it:
We can also inline add_3
: