Skip to content
Gallery
Myk's Digital Garden
Share
Explore
Philosophy

icon picker
Thinking Like a Function

Part 1: What's a function?


As a software engineer, you probably think of a function as a unit of code that takes some arguments and returns some value, eg:
function square(x) {
return x * x;
}
That's a pragmatic definition, but it's not the actual meaning of the word function. A function is a `relation` between a `domain` and a `range` such that for each input there exactly one output specified.
A `relation` is a set of tuples where each tuple shares the same schema - in other words, a relation is a table. A tuple is a row. A value within the tuple is a cell. In this table, our `domain` describes the set of all valid values for our input column, while the `range` describes all valid values for our output column.
So here's a relational representation of the same function as above (or at least the first few rows of it):
Function 1 - Square Input
Input
Output
1
1
1
2
2
4
3
3
9
4
4
16
5
5
25
There are no rows in this table

We now have two different representations of the `square` function - our progamatically defined one has the advantage that it runs, but I'm here to tell you that when you start learning to see functions as relations in addition to seeing them as programming constructs a lot of other stuff starts to make sense.

Part 2: Unlearning I/O

So a function is a relation that maps inputs to outputs in a way that guarantees that the same input will always map to the same output. Ok. So let's look at the following example, and there's going to be a pop quiz at the end:
let x = 0;
// every ten milliseconds change x to a different random value between 0 and 99
setInterval(() => x = Math.floor(Math.random() * 100)), 10)

// assume timestamp() returns some string-formatted representation of Date.now()
function logX() {
console.log(`The value of x at time ${timestamp()} was ${x}`)
}
My question is: is `logX` a function? A lot of smart folks would say no, it's not - that it takes no arguments and returns no value, so how could it possibly represent a relation between input and output?
I'm not here to tell you that those folkks are wrong - but I am here to offer a slightly different perspective. This is just a lens, perhaps something at the level of 'useful metaphor'. I'm here to tell you that `logX` is totally a function, provided you're willing to unlearn what you understand about inputs and outputs. Let's try to model the above relationally to see what we can figure out.
Function 2 - Is It A Function?
x
timestamp
console.log
1
72
1
"The value of x at time 1 was 72”
2
23
2
"The value of x at time 2 was 23”
3
32
3
"The value of x at time 3 was 32”
4
3
4
“The value of x at time 4 was 3”
5
68
5
"The value of x at time 5 was 68”
There are no rows in this table
That's interesting! This is kind of starting to look like a function, right? The only difference is that we haven't labeled any of these columns as inputs or outputs - but there's nothing stopping us from doing that.
We can just say that the INPUT of this relation is {x, timestamp} and the OUTPUT of this relation is whatever gets written to `console.log`. Now our three-column table does in fact represent a function - for every unique combination of timestamp and X there is a specific and unambiguous value output to the console.
The lesson here, for me at least, is just this: when you reason about your function as a relation you have nowhere to specify 'arguments' or 'return values' - you just have columns that hold facts and you can choose to arbitrarily declare that some columns are inputs and some columns are outputs, as long as each input leads to a unique and unambiguous output. Does that make sense?
So when I say you have to unlearn IO, what I mean is this: a function's input is not limited to its arguments, and its output is not limited to its return value. Rather, _a function's input is the set of values available to it_, and a function's output is _the set of changes the function makes to its universe_.
So going back to our JS example above, we can say that `logX` is definitely a function as long as we're willing to concede that `x` and the value of `timestamp()` can be thought of as inputs, and as long as we're willing to model writing out to the console to be a form of output.
Not convinced? Think about Object Oriented Programming, where you have a variable called `this` or `self`, depending on your language. What's the purpose of that variable? Well, two things: (1) it holds values that were computed elsewhere in your class, and (2) it allows methods to mutate it. This is (1) a mechanism for providing non-argument inputs into your function, and (2) a mechanism for allowing your function to make changes to its universe. The whole point of OOP is that grouping stuff together allows certain values to be passed around between methods implicitly, rather than explicitly - but if you understand that, relationally, you can think of `this` as just another column, then you're well on your way to grokking it.

Part 3: Abstraction


Let's go back to our `square` function from the opening:
function square(x) {
return x * x
}
What if we wanted to abstract this function out so that rather than always squaring the input it instead raised the input to the power provided in a second argument?
function raise(x, power) {
return Math.pow(x, power)
}
Kind of a contrived example, but it illustrates an important point: we've added a new input to an existing function, and updated its behavior to make use of that input. What does this look like when we bring it over into a relational representation?
Function 3 - raise x to power
x
power
return value
1
1
2
1
2
2
2
4
3
3
2
9
4
1
3
1
5
2
3
8
There are no rows in this table

Do you see what we just did? We took our original square table and added a new column. Our existing square function still exists as a subset of this table - in fact, if you took only those rows where `power=2` then you'd get something identical to the square function. But by adding a power column we've added a whole new dimension to our function - this is called abstraction, and when you model functions as relations it's easy to see that abstraction is literally just adding a new column to your inputs.
So what does it mean to remove a column?

Part 4: Concretion


This is where stuff gets fun. Let's take our `raise` function and partially apply the first argument:
function raise(x, power) {
return Math.pow(x, power)
}
const raiseTwoByPower = raise.bind(null, 2)
We've created a new function which is a partial application of our initial function - we've filled in the value of `x` to always be 2. Now when you call `raiseTwoByPower` you just have to pass in a single value, the power. To represent this relationally looks something like this:
Function 4 - raise 2 to power (explicit)
x
power
return value
1
2
1
2
2
2
2
4
3
2
3
8
4
2
4
16
5
2
5
32
There are no rows in this table

But honestly, if the value of the `x` column never changes then why even have it there? We no longer need it to form a complete unique input, so we can just drop it and represent our `raiseTwoByPower` as a simple two-column relation:
Function 5 - raise 2 to power (implicit)
power
return value
1
1
2
2
2
4
3
3
8
4
4
16
5
5
32
There are no rows in this table

This is a phenomenally powerful technique, and it works by constraining the `data type` of an input column to a single value. We'll get to what exactly that means in *Part IV*, but first I want to beat you over the head with how important concretion really is and how many ways we do it every day without necessarily realizing that we're doing the same damn thing.

// sometimes we use partial application
const sum = (a, b) => a + b
const addThree = sum.bind(null, 3) // a is now always equal to 3

// sometimes we use currying
const sum a => b => a + b
const addThree = sum(3) // again, a is now always equal to three

// sometimes we use object orientation!
class Adder {
constructor(a) {
this.a = a
}
add(x) {
return this.a + x
}
}
let addThree = new Adder(3).add // same thing!

// sometimes we use mutable global scope!
let addend = 3
const addThree = x => addend + x // again, we get the same relational structure
// (this one is less 'pure' because `a` never existed)
It turns out that concretion is a thing we do all the time, and we do it so unconsciously that here are four different ways to express it in javascript and I bet you never thought of these four things as effectively the same operation, right?
So what exactly are we doing when we add/remove columns? Where do they come from, and where do they go?

Part 5: Types


Reasonable people can disagree on what exactly a Type is in programming - is it a set? Is it a predicate function? Is a type as expressed in one high level language the same as an identical type expressed in a different one?
I'm here to tell you not to worry about those questions. What you need to know about types is that they constrain the set of valid values for each of our columns. Remember how we said that a function is a relation mapping Domain to Range? At a high level, the Domain of a function is the type of its inputs, and the Range of a function is the type of its outputs. So when you see something like this:

const sum = (a, b) => a + b
What are its types?
Well, that's kind of a trick question. Intuitively we want to answer that the domain, or input type, is something like {number, number} and its range, or output type, is just {number}. In practice, though, different languages vary wildly in their capacity to allow you to specify types. With the `sum` function under discussion, for instance, we get a variety of interesting behaviors that can violate our naive assumptions about types -
// sure this works:
sum(2, 2) // 4
// but then we get something like this:
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.