The Functor pattern: transforming a value without unwrapping its container
JavaScript's array .map() and last lesson's Option type look unrelated on the surface — one transforms a list, the other represents presence-or-absence. They're actually the exact same underlying pattern, applied to two different kinds of "container," and naming that pattern explicitly is this lesson's whole point.
Advanced
3 min read
The pattern, stated generally: apply a function INSIDE a container, without removing it from the container
[1, 2, 3].map(lambda x: x * 2) # [2, 4, 6] — still a LIST, transformed in place, conceptually
.map() takes a function meant for a single value (x => x * 2) and applies it to whatever's inside a list, producing a new list of the same shape holding the transformed values — the caller never has to manually pull each element out, transform it, and rebuild the list by hand. A Functor is exactly this pattern, generalized: any container type that provides a map-like operation, letting you transform the value(s) inside it while the container itself stays the same kind of container, just holding a new, transformed value.
Applying the same pattern to last lesson's Option type
class Some: def __init__(self, value): self.value = value def map(self, f): return Some(f(self.value)) # transform the INSIDE value, stay a Someclass NoneOption: def map(self, f): return self # nothing inside to transform — stays None, f never even runsfind_user(42).map(lambda user: user.name)# Some("Ana") if a user was found, or NoneOption() if not —# the caller NEVER has to write an explicit "if it exists" check here at all
Giving Some/NoneOption their own .map() method means .map(lambda user: user.name) works correctly in both cases, automatically — if the option holds a real user, the function runs and the result is wrapped back in Some; if it's NoneOption, .map() simply does nothing and returns itself unchanged, since there's no value inside to apply the function to. This is the genuinely powerful part: the caller writes the transformation once, without an explicit presence check, and the container's own .map() implementation handles both the "something's there" and "nothing's there" cases correctly on its own.
Why recognizing this as "the same pattern" is genuinely useful, not just academic
Once .map() is understood as "transform what's inside, keep the same kind of container" rather than "a specific list method," the same mental model transfers directly to any container-shaped type that provides it — a Promise's .then() (JavaScript's own promises lesson) is recognizably the same pattern again: transform the eventual resolved value, stay a Promise. A reader who's internalized the general Functor pattern recognizes .then(), .map() on an Option, and .map() on an array as three instances of one idea, rather than three unrelated APIs to memorize separately.
The one law a real Functor has to follow
Mapping with a function that does nothing (an identity function, x => x) has to produce an unchanged container back — [1,2,3].map(x => x) must equal [1,2,3], and Some(5).map(x => x) must equal Some(5). This isn't a pedantic technicality — it's what guarantees .map() genuinely only transforms the value inside and never does anything else sneaky to the container's own structure or behavior along the way, which is exactly the property that makes the pattern composable and predictable across every different container type that implements it correctly.