Functors have been with us for a long time while we were producing products. They are like air. They are a very important feature for JavaScript, but you don’t really know if it exists, or don’t realize easily how important it is, despite knowing it exists.
If you already know Haskell or other programming languages that have types, this article won’t be too hard for you. But don’t worry, you don’t have to learn Haskell from the beginning. Just remember that any types this post is going to mention are abstract concepts.
Besides, it’s better if you know what TypeScript is since this concept is widely used in TypeScript — especially in the monads family.
JavaScript was born without types. This language is called a dynamically-typed language or dynamic programming language because the types of variables are determined at runtime.
var name = 'foo';
name = 1;
In this example, the type of name
will be a number, not a string.
Although the name
was assigned as foo
, 1
was assigned to name
later. JavaScript lets developers change the values of variables freely, and it evaluates the types of variables at runtime.
Since ES6, the new variable keyword, const
, has come out. You can’t change the value once the variable is declared with const
. Yet, JavaScript still performs the same as before when there wasn’t const
.
Besides, you can still cheat JavaScript in this way.
const numbers = [1, 2, 3];
numbers.push('4');
Even though numbers
were declared with const
, you can push a string value into it, because what JavaScript restricts on numbers
is what will be stored in the memory space for numbers
, which is an array, not each index’s value.
In other words, this means JavaScript doesn’t care what values are stored in numbers
, as long as numbers
is an array.
As time went by, a new programming language appeared, TypeScript. TypeScript allows you to use many useful types for developing and maintaining your applications.
One of the big differences between JavaScript and TypeScript is that TypeScript never cares about runtime processing. It always says something to you at compile time.
So yes, it actually doesn’t solve the dynamic-typed problem at runtime. However, now you can get a safer way to declare and maintain the variables.
let name: string = 'John';
name = 1;
~~~~~~~~~
// Cannot assign to 'yourName' because it is a constant.
TypeScript helps you avoid wrong typecasting. This still works fine at runtime, but it’s an error at compile-time, which is the period when TypeScript works.
Alright, now let’s think about the way you can express the type of function. In JavaScript, any function can accept any input values and can return any value as the output.
By default, functions return undefined
if there is no return value. Then, I could write the type of function as follows.
type Function = (...args: any[]) => any
Then, let’s assume there is a function that takes a
which is an A
type and returns b
which is a B
type.
type Function = (a: A) => B
In this post, I will express that expression in a slightly different way. The function f
represents the type of it that takes the a
type value and returns the b
type value.
f => (a -> b)
// fat arrow (=>) means (=)
Let’s try to approach this boring concept with a more realistic example.
One day, on a hungry night, you wanted to have a pizza. You drove your car to a pizza store and ordered a pepperoni pizza. In 30 minutes, you get what you ordered.
Oops, you realized that you were going to order extra toppings on it. You asked the worker: “I’m sorry, can I add extra toppings on this?” The worker said: “I’m sorry, I’m a cashier, I can’t help you.”
At that time, someone came out of the kitchen and asked you: “May I help you?” You explained to them what you wanted.
And they said: “You came to the right person. I can do that for you,” and put some extra toppings on your pizza, and put it in the pizza box again. And they gave you the pizza with the box.
“As long as you bring the box with your pizza inside, I can change the pizza to whichever pizza you want.”
In this example, what the person from the kitchen did was take the pizza out of the box, put the toppings on it, and put it back in the box again. Even if you want to get another pizza, all you need to do is just to tell them to change the pizza in the box. They are a functor.
Imagine that they are a function that takes an input and returns an output — a pizza.
KitchenGuy => (pizza A -> pizza B)
So this person is given a pizza A
and gives you a pizza B
. But, what if you want to change pizza B
to pizza C
? Remember what they said?
“As long as you bring the box, I will change it for you.”
We could write the format of KitchenGuy
as follows.
KitchenGuy w/ Box => (pizza A w/ Box -> pizza B w/ Box);
Now let’s talk about a functor. First things first, I will write down the expression of it. It’s OK even if you don’t get what it means, I will explain everything step-by-step, by comparing it to the kitchen person’s story.
fmap:: (Functor f) => (a -> b) -> (fa -> fb)
This format looks very similar to KitchenGuy
. A functor Functor f
takes a function as the argument and returns the function again, but look at the type of the returned value. There’s f
on the very left side of each input and output.
In this format, just like the KitchenGuy
always returned your pizza with the box, a functor also always returns the new function with the container type, f
.
KitchenGuy
accepts the pizza as input, and a factor accepts a function as input. Easy, right? Then, you might wonder what fmap
is on the very left side.
A functor takes the input out of the container (pizza box) and works on it by looping over and putting the output in the container. A functor is also used in Haskell, they have a certain method fmap
, which is called functor.
Let’s look at a simple example of an array’s map
method.
const array = [1, 2, 3];
const newArray = array.map(x => x * 2);
console.log(array) // [1, 2, 3]
console.log(newArray) // [2, 4, 6]
The map
takes a function that loops over the array and it returns a new array. Doesn’t it sound quite familiar now?
Yes, you are right. An array is a functor. Why? Because you can keep chaining map
s. Like the KitchenGuy
can work on the pizza in the pizza box over and over again as long as they receive the pizza with the box, a functor returns another functor.
A functor is a basic concept of functional programming that you should know if you are interested.
Many people think and feel it’s very complicated and difficult to understand but it is not. The only reason it confuses you is that it always comes with types, which is like an alien language for newbies like me.
The primary key point you should remember is that a functor is just a container of some values inside and it returns another functor. A functor loops over the values inside after taking them out of the container, so whatever you want to be a functor, it should be iterable.
I prepared a short example class
of a functor.
class Functor {
value = null;
constructor(value) {
this.value = value;
}
map(fn) {
return new Functor(fn(this.value));
}
}
Here’s a class Functor
. It has map
, which takes a function that takes a value. What map
does in this example is to execute fn
by passing this.value
into it and creating a new Functor
class.
new Functor({ a: 1 });
// Functor { a: 1 }
new Functor({ a: 1})
.map(value => {
value.b = 2;
return value;
});
// Functor { a: 1, b: 2 }
Like this, you can keep chaining map
s with this Functor
class.
Thank you for reading!
#programming #javascript #webdev #coding #functional programming