DEV Community

Cover image for You've Been Using Functors For Years
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

You've Been Using Functors For Years

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then somebody attempts to explain those words using even scarier words.

Which leads to:

Confusion
↓
Frustration
↓
Closing the browser tab
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Nothing surprising.

Now let's look at Promises.

Promise.resolve(10)
  .then(x => x * 2)
Enter fullscreen mode Exit fullscreen mode

Now RxJS.

userStream.pipe(
  map(user => user.name)
)
Enter fullscreen mode Exit fullscreen mode

And Option types.

Option.some(10)
  .map(x => x * 2)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That's not actually what map does.

The deeper idea is:

Take a value
Transform it
Preserve its container
Enter fullscreen mode Exit fullscreen mode

Let's look at Arrays.

Input:

[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Transformation:

x => x * 2
Enter fullscreen mode Exit fullscreen mode

Output:

[2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Notice something important.

The values changed.

The Array did not.

Array<Number>
↓
Transform
↓
Array<Number>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Input:

Promise<Number>
Enter fullscreen mode Exit fullscreen mode

Output:

Promise<Number>
Enter fullscreen mode Exit fullscreen mode

Again:

Container
↓
Transformation
↓
Same Container
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

Input:

Observable<User>
Enter fullscreen mode Exit fullscreen mode

Output:

Observable<String>
Enter fullscreen mode Exit fullscreen mode

Again:

Container
↓
Transformation
↓
Same Container
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then this article teaches:

Functor
=
Transformation While Preserving Context
Enter fullscreen mode Exit fullscreen mode

Or:

Container<Value>
↓
Transformation
↓
Container<NewValue>
Enter fullscreen mode Exit fullscreen mode

That's the pattern.


A Practical Example

Suppose we receive:

const users = [
  {
    id: 1,
    name: "John"
  },
  {
    id: 2,
    name: "Sarah"
  }
]
Enter fullscreen mode Exit fullscreen mode

We only need names.

const names = users.map(
  user => user.name
)

console.log(names)
Enter fullscreen mode Exit fullscreen mode

Output:

[
  "John",
  "Sarah"
]
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

Frontend dropdown:

const options = countries.map(
  country => ({
    label: country.name,
    value: country.code
  })
)
Enter fullscreen mode Exit fullscreen mode

Output:

[
  {
    label: "United Arab Emirates",
    value: "AE"
  },
  {
    label: "India",
    value: "IN"
  }
]
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Underneath:

Array<User>
↓
Transform
↓
Array<JSX>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

API responses become UI models.

JSON
↓
Transform
↓
View Model
Enter fullscreen mode Exit fullscreen mode

View models become UI.

Model
↓
Transform
↓
Component
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

the result should equal:

container
Enter fullscreen mode Exit fullscreen mode

Example:

[1, 2, 3]
  .map(x => x)
Enter fullscreen mode Exit fullscreen mode

Output:

[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Makes sense.


Law #2: Composition

This:

container
  .map(f)
  .map(g)
Enter fullscreen mode Exit fullscreen mode

should be equivalent to:

container.map(
  x => g(f(x))
)
Enter fullscreen mode Exit fullscreen mode

Example:

const double = x => x * 2
const square = x => x * x
Enter fullscreen mode Exit fullscreen mode

Version 1:

[1, 2, 3]
  .map(double)
  .map(square)
Enter fullscreen mode Exit fullscreen mode

Version 2:

[1, 2, 3]
  .map(x =>
    square(double(x))
  )
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

creates intermediate arrays.

Each map allocates memory.

For small datasets:

No problem.

For large datasets:

Potential issue.

Example:

const result = users
  .map(...)
  .map(...)
  .map(...)
Enter fullscreen mode Exit fullscreen mode

Internally:

Array
↓
Array
↓
Array
↓
Array
Enter fullscreen mode Exit fullscreen mode

Multiple allocations.

Multiple passes.


Loops Still Matter

A simple loop:

const result = []

for (const user of users) {
  result.push(
    user.name.toUpperCase()
  )
}
Enter fullscreen mode Exit fullscreen mode

is often:

  • Faster
  • Easier to debug
  • Easier to profile

That does not make map bad.

It simply means tradeoffs exist.

map()
↓
Clarity
Composability
Enter fullscreen mode Exit fullscreen mode

versus

for...of
↓
Control
Performance
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

or

promise.then(...)
Enter fullscreen mode Exit fullscreen mode

or

observable.pipe(map(...))
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

And that question leads us directly to:

flatMap()
Monads
Composition
Enter fullscreen mode Exit fullscreen mode

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)