Playing around with Closures, Currying, and Cool Abstractions
TK
Posted on March 8, 2020
This article was first published at the TK's blog.
In this article, we will talk about closures, curried functions, and play around with these concepts to build cool abstractions. I want to show the idea behind each concept, but also make it very practical with examples and refactor code to make it more fun.
Closures
So closure is a common topic in JavaScript and we will start with it. As MDN web docs defines:
"A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment)."
Basically, every time a function is created, a closure is also created and it gives access to all state (variables, constants, functions, etc). The surrounding state is known as the lexical environment
.
Let's show a simple example:
function makeFunction() {
const name = 'TK';
function displayName() {
console.log(name);
}
return displayName;
};
What do we have here?
- Our main function called
makeFunction
- A constant named
name
assigned with a string'TK'
- The definition of the
displayName
function (that just log thename
constant) - And finally the
makeFunction
returns thedisplayName
function
This is just a definition of a function. When we call the makeFunction
, it will create everything within it: constant and function in this case.
As we know, when the displayName
function is created, the closure is also created and it makes the function aware of the environment, in this case, the name
constant. This is why we can console.log
the name
without breaking anything. The function knows about the lexical environment.
const myFunction = makeFunction();
myFunction(); // TK
Great! It works as expected! The return of the makeFunction
is a function that we store it in the myFunction
constant, call it later, and displays TK
.
We can also make it work as an arrow function:
const makeFunction = () => {
const name = 'TK';
return () => console.log(name);
};
But what if we want to pass the name and display it? A parameter!
const makeFunction = (name = 'TK') => {
return () => console.log(name);
};
// Or a one-liner
const makeFunction = (name = 'TK') => () => console.log(name);
Now we can play with the name:
const myFunction = makeFunction();
myFunction(); // TK
const myFunction = makeFunction('Dan');
myFunction(); // Dan
Our myFunction
is aware of the arguments passed: default or dynamic value.
The closure does make the created function not only aware of constants/variables, but also other functions within the function.
So this also works:
const makeFunction = (name = 'TK') => {
const display = () => console.log(name);
return () => display();
};
const myFunction = makeFunction();
myFunction(); // TK
The returned function knows about the display
function and it is able to call it.
One powerful technique is to use closures to build "private" functions and variables.
Months ago I was learning data structures (again!) and wanted to implement each one. But I was always using the object oriented approach. As a functional programming enthusiast, I wanted to build all the data structures following FP principles (pure functions, immutability, referential transparency, etc).
The first data structure I was learning was the Stack. It is pretty simple. The main API is:
-
push
: add an item to the first place of the stack -
pop
: remove the first item from the stack -
peek
: get the first item from the stack -
isEmpty
: verify if the stack is empty -
size
: get the number of items the stack has
We could clearly create a simple function to each "method" and pass the stack data to it. It use/transform the data and return it.
But we can also create a private stack data and exposes only the API methods. Let's do this!
const buildStack = () => {
let items = [];
const push = (item) => items = [item, ...items];
const pop = () => items = items.slice(1);
const peek = () => items[0];
const isEmpty = () => !items.length;
const size = () => items.length;
return {
push,
pop,
peek,
isEmpty,
size,
};
};
As we created the items
stack data inside our buildStack
function, it is "private". It can be accessed only within the function. In this case, only the push
, pop
, etc could touch the data. And this is what we're looking for.
And how do we use it? Like this:
const stack = buildStack();
stack.isEmpty(); // true
stack.push(1); // [1]
stack.push(2); // [2, 1]
stack.push(3); // [3, 2, 1]
stack.push(4); // [4, 3, 2, 1]
stack.push(5); // [5, 4, 3, 2, 1]
stack.peek(); // 5
stack.size(); // 5
stack.isEmpty(); // false
stack.pop(); // [4, 3, 2, 1]
stack.pop(); // [3, 2, 1]
stack.pop(); // [2, 1]
stack.pop(); // [1]
stack.isEmpty(); // false
stack.peek(); // 1
stack.pop(); // []
stack.isEmpty(); // true
stack.size(); // 0
So, when the stack is created, all the functions are aware of the items
data. But outside the function, we can't access this data. It's private. We just modify the data by using the stack builtin API.
Curry
"Currying is the process of taking a function with multiple arguments and turning it into a sequence of functions each with only a single argument." - Wikipedia
So imagine you have a function with multiple arguments: f(a, b, c)
. Using currying, we achieve a function f(a)
that returns a function g(b)
the returns a function h(c)
.
Basically: f(a, b, c)
—> f(a) => g(b) => h(c)
Let's build a simple example: add two numbers. But first, without currying!
const add = (x, y) => x + y;
add(1, 2); // 3
Great! Super simple! Here we have a function with two arguments. To transform it into a curried function we need a function that receives x
and returns a function that receives y
and returns the sum of both values.
const add = (x) => {
function addY(y) {
return x + y;
}
return addY;
};
We can refactor this addY
into a anonymous arrow function:
const add = (x) => {
return (y) => {
return x + y;
}
};
Or simplify it by building one liner arrow functions:
const add = (x) => (y) => x + y;
These three different curried functions have the same behavior: build a sequence of functions with only one argument.
How we use it?
add(10)(20); // 30
At first, it can look a bit strange, but it has a logic behind it. add(10)
returns a function. And we call this function with the 20
value.
This is the same as:
const addTen = add(10);
addTen(20); // 30
And this is interesting. We can generate specialized functions by calling the first function. Imagine we want an increment
function. We can generate it from our add
function by passing the 1
as the value.
const increment = add(1);
increment(9); // 10
When I was implementing the Lazy Cypress, a npm library to record the user behavior in a form page and generate Cypress testing code, I want to build a function to generate this string input[data-testid="123"]
. So here we have the element (input
), the attribute (data-testid
), and the value (123
). Interpolating this string in JavaScript would look like this: ${element}[${attribute}="${value}"]
.
the first implementation in mind is to receive these three values as parameters and return the interpolated string above.
const buildSelector = (element, attribute, value) =>
`${element}[${attribute}="${value}"]`;
buildSelector('input', 'data-testid', 123); // input[data-testid="123"]
And it is great. I achieved what I was looking for. But at the same time, I wanted to build a more idiomatic function. Something I could write "get an element X with attribute Y and value Z". So what if we break this phrase into three steps:
- "get an element X":
get(x)
- "with attribute Y":
withAttribute(y)
- "and value Z":
andValue(z)
We can transform the buildSelector(x, y, z)
into get(x)
⇒ withAttribute(y)
⇒ andValue(z)
by using the currying concept.
const get = (element) => {
return {
withAttribute: (attribute) => {
return {
andValue: (value) => `${element}[${attribute}="${value}"]`,
}
}
};
};
Here we use a different idea: returning an object with function as key-value. This way we can achieve this syntax: get(x).withAttribute(y).andValue(z)
.
And for each returned object, we have the next function and argument.
Refactoring time! Remove the return
statements:
const get = (element) => ({
withAttribute: (attribute) => ({
andValue: (value) => `${element}[${attribute}="${value}"]`,
}),
});
I think it looks prettier. And we use it like:
const selector = get('input')
.withAttribute('data-testid')
.andValue(123);
selector; // input[data-testid="123"]
The andValue
function knows about the element
and attribute
values because it is aware of the lexical environment as we talked about closures before.
We can also implement functions using "partial currying". Separate only the first argument from the rest for example.
Doing web development for a long time, I commonly used the event listener Web API. It is used this way:
const log = () => console.log('clicked');
button.addEventListener('click', log);
I wanted to create an abstraction to build specialized event listeners and use them by passing the element and callback handler.
const buildEventListener = (event) => (element, handler) => element.addEventListener(event, handler);
This way I can create different specialized event listeners and use it as functions.
const onClick = buildEventListener('click');
onClick(button, log);
const onHover = buildEventListener('hover');
onHover(link, log);
With all these concepts, I could create a SQL query using JavaScript syntax. I wanted to SQL query a JSON data like:
const json = {
"users": [
{
"id": 1,
"name": "TK",
"age": 25,
"email": "tk@mail.com"
},
{
"id": 2,
"name": "Kaio",
"age": 11,
"email": "kaio@mail.com"
},
{
"id": 3,
"name": "Daniel",
"age": 28,
"email": "dani@mail.com"
}
]
}
So I built a simple engine to handle this implementation:
const startEngine = (json) => (attributes) => ({ from: from(json, attributes) });
const buildAttributes = (node) => (acc, attribute) => ({ ...acc, [attribute]: node[attribute] });
const executeQuery = (attributes, attribute, value) => (resultList, node) =>
node[attribute] === value
? [...resultList, attributes.reduce(buildAttributes(node), {})]
: resultList;
const where = (json, attributes) => (attribute, value) =>
json
.reduce(executeQuery(attributes, attribute, value), []);
const from = (json, attributes) => (node) => ({ where: where(json[node], attributes) });
With this implementation, we can start the engine with the JSON data:
const select = startEngine(json);
And use it like a SQL query:
select(['id', 'name'])
.from('users')
.where('id', 1);
result; // [{ id: 1, name: 'TK' }]
That's it for today. We could go on and on showing a lot of different examples of abstractions, but now I let you play with those concepts.
Resources
Posted on March 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.