The functional programming community has done a terrible job explaining Functors.
The moment the word appears, people start talking about:
- Categories
- Morphisms
- Endofunctors
- Higher-Kinded Types
- Laws
- Haskell
Most developers immediately close the tab.
And honestly, I don't blame them.
Because Functors are often introduced in the most complicated way possible.
Which is unfortunate because Functors are one of the simplest ideas in programming.
In fact, if you've ever written:
[1, 2, 3].map(x => x * 2)
then congratulations.
You've already used a Functor.
You just didn't know it.
The Functional Programming Trap
Most developers learn functional programming backwards.
They start with scary words.
Functor
Monad
Applicative
Category Theory
Then somebody attempts to explain those words using even scarier words.
Which leads to:
Confusion
↓
Frustration
↓
Closing the browser tab
I think the opposite approach works much better.
Instead of starting with the name, let's start with something every JavaScript developer already understands.
map().
Why Does map() Keep Appearing Everywhere?
Let's start with Arrays.
const result = [1, 2, 3]
.map(x => x * 2)
console.log(result)
// [2, 4, 6]
Nothing surprising.
Now let's look at Promises.
Promise.resolve(10)
.then(x => x * 2)
Now RxJS.
userStream.pipe(
map(user => user.name)
)
And Option types.
Option.some(10)
.map(x => x * 2)
Different libraries.
Different authors.
Different purposes.
Yet they all keep reinventing the same operation.
Why?
The Pattern Hidden Behind map()
Most developers think:
map()
=
Loop through an array
That's not actually what map does.
The deeper idea is:
Take a value
Transform it
Preserve its container
Let's look at Arrays.
Input:
[1, 2, 3]
Transformation:
x => x * 2
Output:
[2, 4, 6]
Notice something important.
The values changed.
The Array did not.
Array<Number>
↓
Transform
↓
Array<Number>
The container stayed intact.
Only the contents changed.
That is the real idea behind map.
Arrays Didn't Invent map()
Now let's revisit Promises.
Promise.resolve(10)
.then(x => x * 2)
Input:
Promise<Number>
Output:
Promise<Number>
Again:
Container
↓
Transformation
↓
Same Container
The Promise remains a Promise.
Only the value changes.
That should feel familiar.
Because it is exactly the same pattern.
RxJS Is Doing The Same Thing
userStream.pipe(
map(user => user.name)
)
Input:
Observable<User>
Output:
Observable<String>
Again:
Container
↓
Transformation
↓
Same Container
The Observable stays an Observable.
The values change.
Same idea.
Different implementation.
So What Is A Functor?
Here's the scary definition.
A Functor is:
A structure that can be mapped over.
That's it.
No category theory required.
No PhD required.
No Haskell required.
If a structure supports value transformation while preserving the structure itself, it behaves like a Functor.
Arrays do this.
Promises do this.
Observables do this.
Option types do this.
Result types do this.
You already use them every day.
The Formula
If the previous article taught us:
Reduce
=
State Evolution
Then this article teaches:
Functor
=
Transformation While Preserving Context
Or:
Container<Value>
↓
Transformation
↓
Container<NewValue>
That's the pattern.
A Practical Example
Suppose we receive:
const users = [
{
id: 1,
name: "John"
},
{
id: 2,
name: "Sarah"
}
]
We only need names.
const names = users.map(
user => user.name
)
console.log(names)
Output:
[
"John",
"Sarah"
]
The Array remains an Array.
Only the contents changed.
Functor behavior.
Another Example: API Responses
Backend:
const countries = [
{
code: "AE",
name: "United Arab Emirates"
},
{
code: "IN",
name: "India"
}
]
Frontend dropdown:
const options = countries.map(
country => ({
label: country.name,
value: country.code
})
)
Output:
[
{
label: "United Arab Emirates",
value: "AE"
},
{
label: "India",
value: "IN"
}
]
Transform values.
Preserve container.
Same pattern.
React Developers Use Functors Daily
Most React developers never think about this.
function UserList({ users }) {
return (
<ul>
{
users.map(user => (
<li key={user.id}>
{user.name}
</li>
))
}
</ul>
)
}
Underneath:
Array<User>
↓
Transform
↓
Array<JSX>
Still the same idea.
The Hidden Relationship Between Functors and Software
Think about what software does all day.
Database rows become API responses.
Rows
↓
Transform
↓
JSON
API responses become UI models.
JSON
↓
Transform
↓
View Model
View models become UI.
Model
↓
Transform
↓
Component
Software is mostly transformation.
Which is why Functors keep appearing everywhere.
Functor Laws (The Only Theory We'll Discuss)
Now that you already understand Functors, we can talk about the famous laws.
Don't panic.
They're surprisingly reasonable.
Law #1: Identity
If you transform nothing:
container.map(x => x)
the result should equal:
container
Example:
[1, 2, 3]
.map(x => x)
Output:
[1, 2, 3]
Makes sense.
Law #2: Composition
This:
container
.map(f)
.map(g)
should be equivalent to:
container.map(
x => g(f(x))
)
Example:
const double = x => x * 2
const square = x => x * x
Version 1:
[1, 2, 3]
.map(double)
.map(square)
Version 2:
[1, 2, 3]
.map(x =>
square(double(x))
)
Same result.
Libraries depend on these guarantees.
Why JavaScript Developers Should Care
Not because you'll suddenly become a functional programming enthusiast.
Not because you'll start writing Haskell.
Because understanding Functors helps explain APIs that you already use.
Once you see:
Array.map()
Promise.then()
Observable.map()
as the same abstraction, many libraries start making more sense.
You stop memorizing APIs.
You start recognizing patterns.
That is where real learning happens.
Performance Considerations
Let's be honest.
This:
array
.map(...)
.map(...)
.map(...)
.map(...)
creates intermediate arrays.
Each map allocates memory.
For small datasets:
No problem.
For large datasets:
Potential issue.
Example:
const result = users
.map(...)
.map(...)
.map(...)
Internally:
Array
↓
Array
↓
Array
↓
Array
Multiple allocations.
Multiple passes.
Loops Still Matter
A simple loop:
const result = []
for (const user of users) {
result.push(
user.name.toUpperCase()
)
}
is often:
- Faster
- Easier to debug
- Easier to profile
That does not make map bad.
It simply means tradeoffs exist.
map()
↓
Clarity
Composability
versus
for...of
↓
Control
Performance
Choose appropriately.
Pros Of Functors
1. Predictable
The container stays the same.
Only the value changes.
2. Composable
Transformations chain naturally.
users
.map(...)
.map(...)
.map(...)
3. Reusable
Transformations become portable.
4. Common Across Ecosystems
Arrays.
Promises.
RxJS.
Streams.
Options.
Results.
Same idea.
5. Easier To Reason About
Transform value.
Preserve context.
Simple mental model.
Cons Of Functors
1. Terrible Naming
The word "Functor" scares people unnecessarily.
2. Easy To Over-Academize
Many explanations make a simple idea look difficult.
3. Not Always Optimal
Repeated mapping can create allocations.
4. Can Encourage Over-Chaining
Huge transformation pipelines become difficult to follow.
5. Sometimes A Loop Is Better
Not every transformation needs a functional abstraction.
The Real Lesson
The funny thing about Functors is that they're not difficult.
The word is difficult.
The idea is simple.
If you've ever used:
array.map(...)
or
promise.then(...)
or
observable.pipe(map(...))
you've already been using Functors.
The only thing this article changed was giving a name to a pattern you already understood.
And once you start recognizing patterns instead of memorizing APIs, software suddenly becomes much easier to learn.
What's Next?
In the next article, we'll discuss something even more important:
FlatMap Is More Important Than You Think
Because once developers understand map(), the next natural question becomes:
What happens when my transformation
returns another container?
And that question leads us directly to:
flatMap()
Monads
Composition
without the usual confusion.
About The Author
Hi, I'm Amrish Khan.
I enjoy building developer tools, exploring software architecture, and writing about the deeper ideas behind everyday programming concepts.
I'm also building Aruvix — a growing ecosystem of local-first developer tools designed to process data directly in the browser without unnecessary uploads.
Here's a detailed blog on Aruvix:
https://dev.to/amrishkhan05/aruvix-the-ultimate-offline-first-developer-toolkit-e0i
You can follow my work and thoughts here:
Portfolio:
https://www.amrishkhan.dev
LinkedIn:
https://www.linkedin.com/in/amrishkhan
GitHub:
https://www.github.com/amrishkhan05
If you enjoyed this article, consider following for more deep dives into JavaScript, architecture, local-first software, and performance engineering.
Top comments (0)