THINKING IN FUNCTIONS
1. Functions are simple
Functions are fundamentally simple. Every function can be thought of as a mapping from one input to one output.
đź’ˇ Tip: whenever you read a function in any language, try to mentally visualize it as an arrow between types.
Functions that take multiple arguments
Let’s look at an example in JavaScript:
let add = (a, b) => a + b
Imagine if it were written like this:
let add = a => b => a + b
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:
let add = a => b => a + b
let add_5 = add(5)
let x = 13
let y = add_5(x)
console.log(y) // 18
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:
let add = a => b => a + b
type add = (a: number) => (b: number) => number
Here is the equivalent in OCaml:
let add = fun a -> fun b -> a + b
type add = int -> int -> int
(* shorthand syntax *)
let add a b = a + b
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
: takes an int, returns an intint -> int -> int
: takes two ints, returns an intint -> int -> int -> int
: takes three ints, returns an 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:
// TypeScript
let foo = () => 42
(* OCaml *)
let foo = fun () -> 42
(* shorthand syntax *)
let foo () = 42
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
:
type foo = () => number
type foo = unit -> int
đź’ˇ 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:
let sayHello = (name) => console.log(`Hello, ${name}!`)
type sayHello = (name: string) => void
let say_hello = fun name -> print_endline ("Hello, " ^ name ^ "!")
type say_hello = string -> unit
đź’ˇ TypeScript uses the type void
to express side effects with no return value, whereas OCaml uses the type unit
.
2. Functions are everywhere
let add = (a, b) => a + b
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:
let three = (+) 1 2
let also_three = 1 + 2
Another way to think about it:
let add = (+)
let three = add 1 2
let also_three = 1 + 2
đź’ˇ 1 + 2
is called infix notation
đź’ˇ (+) 1 2
is known as prefix notation or Polish notation
Same with multiplication:
let multiply = ( * )
let six = multiply 2 3
let also_six = 2 * 3
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:
let numbers = [1; 2; 3; 4; 5] in
let add = fun a -> fun b -> a + b in
let result = List.map (fun n -> add 3 n) numbers in
()
💡 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:
let numbers = [1; 2; 3; 4; 5] in
let add = fun a -> fun b -> a + b in
let add_3 = add 3 in
let result = List.map add_3 numbers in
()
Also recall that +
is a function, so we can use it like this:
let numbers = [1; 2; 3; 4; 5] in
let add = (+) in
let add_3 = add 3 in
let result = List.map add_3 numbers in
()
Now add
is basically just an alias for (+)
, so we can inline it:
let numbers = [1; 2; 3; 4; 5] in
let add_3 = (+) 3 in
let result = List.map add_3 numbers in
()
We can also inline add_3
:
let numbers = [1; 2; 3; 4; 5] in
let result = List.map ((+) 3) numbers in
()
3. 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:
let numbers = [1; 2; 3; 4; 5] in
let add_three = (+) 3 in
let greater_than_five = fun x -> x > 5 in
let map_add_three = List.map add_three in
let filter_greater_than_five = List.filter greater_than_five in
let sum = List.fold_left (+) 0 in
let result = sum (filter_greater_than_five (map_add_three numbers))
Using OCaml’s |>
pipe operator, we can make this more readable by chaining the operations into a pipeline:
let numbers = [1; 2; 3; 4; 5] in
let add_three = (+) 3 in
let greater_than_five = fun x -> x > 5 in
let sum = List.fold_left (+) 0 in
let result = numbers
|> List.map add_three
|> List.filter greater_than_five
|> sum
in
()
Let’s inline some of the expressions:
let result = [1; 2; 3; 4; 5]
|> List.map ((+) 3)
|> List.filter (fun x -> x > 5)
|> List.fold_left (+) 0
in
()
Equivalent program in JavaScript:
let result = [1, 2, 3, 4, 5]
.map(x => x + 3)
.filter(x => x > 5)
.reduce((acc, x) => acc + x, 0)
đź’ˇ The pipe operator in OCaml takes a value on the left and passes it as the last argument to the function on the right.
SUMMARY
- Functions are simple
- Functions take one input and return one output.
- Functions conceptually work the same way regardless of language or syntax.
- A function is a mapping from an input type to an output type.
- Functions are everywhere
- Operators are functions
- Functions are composable
- Pipe operator
|>
is useful for piping values through a series of functions.
- Pipe operator