The Dreaded Three Link to heading

Happy new year! Good to be back. What better way to kickstart this year than to scare you to death with the three most dreaded words in computer science since NullPointerException?

If you’ve spent any time around functional programming communities, you’ve encountered them. Elodin in The Kingkiller Chronicles claims there are Seven Words to make a woman love you, and there are definitely three words that make junior developers’ eyes glaze over. The concepts that spawn a thousand Medium articles with titles like “Monads Explained with Burritos” (not kidding). The triforce of functional programming jargon: Functors, Applicatives, and Monads.

These terms have acquired an almost mythical status. They’re spoken of in hushed tones, accompanied by warnings about category theory and abstract algebra. People act like you need a mathematics PhD just to understand what’s happening when you chain some operations together.

Here’s the thing, though: if you’ve written any Elm code at all, you already use these concepts daily. You just don’t call them by their scary names.

Lo and behold, this might be the day where you finally Get It. Read on!

What’s Actually Going On Link to heading

Let me let you in on a secret: these three concepts are just patterns for working with “wrapped” values. That’s it. Values in containers. Values in boxes.1 Whatever mental image helps you.

Think about Maybe in Elm. It’s a container that might hold a value, or might be empty:

-- A value in a "maybe there, maybe not" container
userName : Maybe String
userName = Just "Christian"

-- An empty container
userAge : Maybe Int
userAge = Nothing

The scary words are just names for different ways of working with these containers.

And if you’ve written JavaScript? You’ve used these too. [1,2,3].map(x => x * 2) is functor territory. fetch().then(res => res.json()).then(data => ...) is (roughly speaking) monadic chaining – then flattens nested Promises just like andThen flattens nested Maybes. The patterns are everywhere; only the vocabulary changes.

Functor: “I Have a Function and a Wrapped Value” Link to heading

A Functor is anything that can be “mapped over.” You have a value in a container, and you want to apply a function to it without unwrapping it first.

In Elm, you do this constantly:

-- Apply a function to a wrapped value
Maybe.map String.toUpper (Just "hello")  -- Just "HELLO"
Maybe.map String.toUpper Nothing         -- Nothing

List.map String.toUpper ["hello", "world"]  -- ["HELLO", "WORLD"]

That’s it. That’s functors. If you can map over it, it’s a functor.

In Haskell, there’s a generic fmap that works on any functor:

-- Haskell: one function to rule them all
fmap toUpper (Just "hello")   -- Just "HELLO"
fmap toUpper ["hello"]        -- ["HELLO"]

Elm doesn’t have this. In Elm, you explicitly say which container you’re mapping over: Maybe.map, List.map, Result.map, and so on. More typing, but also more clarity about what’s actually happening.

Applicative: “My Function Takes Multiple Arguments” Link to heading

Functors work great when your function takes one argument. But what about two?

-- User takes two arguments
type alias User = { name : String, age : Int }

-- We have two wrapped values
maybeName : Maybe String
maybeName = Just "Alice"

maybeAge : Maybe Int
maybeAge = Just 30

-- Let's try mapping...
Maybe.map User maybeName
-- Result: Just <function>
-- Type: Maybe (Int -> User)

Wait. We mapped a two-argument function, but we only gave it one argument. Now we have a function stuck inside a Maybe. And we still have maybeAge sitting there, also wrapped.

Regular map can’t help us. It applies a normal function to a wrapped valueβ€”but now our function is also wrapped.

This is where Applicatives come in. In Haskell, you’d use <*> to apply a wrapped function to a wrapped value:

-- Haskell: apply the wrapped function to another wrapped value
Just (User "Alice") <*> Just 30  -- Just (User "Alice" 30)
Nothing <*> Just 30              -- Nothing

In Elm, you skip the intermediate step entirely with map2:

-- Elm: combine two wrapped values with a two-argument function
Maybe.map2 User maybeName maybeAge  -- Just { name = "Alice", age = 30 }

The pattern scales: map3 for three arguments, map4 for four, and so on. If any of the values is Nothing, the whole thing short-circuits to Nothing.

Monad: “I Have a Wrapped Value, and a Function That Returns a Wrapped Value” Link to heading

Now for the big one. The M-word. The concept that has launched countless terrible tutorials.

A Monad handles this situation: you have a wrapped value, and you want to apply a function that itself returns a wrapped value. Without special handling, you’d end up with Maybe (Maybe a) – a wrapped wrapped value. Nobody wants that.

Literally nobody! Wrapped wrapped wrapped

In Elm, this is andThen:

-- A function that returns a Maybe
parseAge : String -> Maybe Int
parseAge str =
    String.toInt str
        |> Maybe.andThen (\n -> if n > 0 then Just n else Nothing)

-- Chain operations that might fail
getUserAge : Maybe String -> Maybe Int
getUserAge maybeAgeString =
    maybeAgeString
        |> Maybe.andThen parseAge

If maybeAgeString is Nothing, the whole chain short-circuits. If parseAge returns Nothing, same thing. The “wrapped wrapped value” problem never materializes because andThen handles the unwrapping for you.

In Haskell, this is the famous >>= (pronounced “bind”):

-- Haskell: the monad bind operator
Just "42" >>= parseAge  -- Just 42
Nothing >>= parseAge    -- Nothing

Again, same concept, different syntax.

Why Elm Doesn’t Use These Terms Link to heading

Here’s what I find fascinating: Elm deliberately avoids the category theory vocabulary entirely. The official Elm guide never mentions functors, applicatives, or monads. Not once.

Instead, you just learn that Maybe.map transforms values in a Maybe, that Maybe.map2 combines two Maybe values, and that Maybe.andThen chains operations that might fail. Practical, concrete, no scary words required.

And honestly? It works beautifully. I’ve introduced Elm to developers who had zero FP background, and they picked up these patterns in hours. The concepts aren’t hard – it’s the terminology that creates the barrier.

The Haskell Type Class Advantage (And Why Elm Says No Thanks) Link to heading

In Haskell, Functor, Applicative, and Monad are type classes – interfaces that let you write generic code:

-- Haskell: this works on ANY functor
doubleInContext :: (Functor f) => f Int -> f Int
doubleInContext = fmap (* 2)

-- Works on Maybe
doubleInContext (Just 5)  -- Just 10

-- Works on List
doubleInContext [1, 2, 3]  -- [2, 4, 6]

-- Works on Either
doubleInContext (Right 5)  -- Right 10

This is powerful. One function, infinite containers.

Elm explicitly rejects this approach. Every container gets its own map, map2, andThen. No generics. No user-defined type classes. Want to map over a Maybe? Maybe.map. A List? List.map. A Result? Result.map.

(Technically, Elm does have a few built-in type classes like comparable and number – that’s how List.sort works on both List Int and List String. But you can’t define your own, and Functor isn’t one of them.)

Why? The Elm philosophy values explicitness over abstraction. When you read Maybe.map, you know exactly what container you’re working with. There’s no wondering “wait, which functor instance is this using?” The code is more verbose but harder to misread.

Is this the right trade-off? Depends on who you ask. But it does mean Elm code tends to be readable by anyone who knows the language, without requiring deep type system knowledge.

The Practical Pipeline Link to heading

Let me show you what this looks like in real Elm code. Say you’re parsing JSON for a user:

type alias User =
    { name : String
    , age : Int
    , email : String
    }

-- Three wrapped values, one three-argument function: applicative!
userDecoder : Decoder User
userDecoder =
    Json.Decode.map3 User
        (field "name" string)   -- Decoder String
        (field "age" int)       -- Decoder Int
        (field "email" string)  -- Decoder String

That map3 is applicative functor stuff. You’re applying a 3-argument function (User) to three wrapped values (the decoders). If any decoding fails, the whole thing fails.

Or consider form validation with monadic chaining:

validateEmail : String -> Result String String
validateEmail input =
    if String.contains "@" input then
        Ok input
    else
        Err "Invalid email"

validateAge : String -> Result String Int
validateAge input =
    String.toInt input
        |> Result.fromMaybe "Age must be a number"
        |> Result.andThen (\n ->
            if n > 0 && n < 150 then
                Ok n
            else
                Err "Age must be between 1 and 149"
        )

That Result.andThen is monadic bind. You’re chaining operations where each step might fail, and the failure automatically propagates.

The Takeaway Link to heading

Look, I get it. “Functor,” “Applicative,” and “Monad” sound intimidating. They come from category theory, they have mathematical definitions, and some explanations make them seem impossibly abstract.

But the concepts are simple:

  • Functor: Apply a function to a wrapped value (map)
  • Applicative: Combine multiple wrapped values with a function (map2, map3, etc.)
  • Monad: Chain operations that return wrapped values (andThen)

(Am I oversimplifying? A little, yes. There are laws these structures must satisfy, edge cases worth knowing, and nuances I’ve glossed over. But here’s the thing: it’s much easier to learn the details once the overview isn’t intimidating and foggy. Start with the intuition, then fill in the formalism as needed.)

You’ve been using these patterns all along. Elm just had the good sense not to scare you with the vocabulary.

And maybe that’s the real lesson here: sometimes the barrier to understanding isn’t the concept – it’s the name we gave it. Strip away the academic terminology, and you’re left with practical patterns for handling values that might not exist, operations that might fail, and side effects that need managing.

Next time someone starts going on about the monad laws or the functor hierarchy, you can smile and nod, knowing that it’s really just map and andThen wearing a fancy hat.


If you want to go deeper, Adit Bhargava’s illustrated guide is genuinely excellent and way more visual than this post. And if you want the real mathematical definitions, the Haskell wiki has you covered – though you absolutely don’t need it to write good functional code.


  1. The “container” or “box” metaphor is helpful for building intuition, but it’s not completely accurate. A more precise term is computational context. Maybe represents a computation that might fail. List represents a computation with multiple possible results. Result represents a computation that might fail with an error message. The context isn’t just “holding” a value – it’s describing how the computation behaves. This distinction matters more as you go deeper: in Haskell, even functions themselves are functors (the (->) r instance), and trying to imagine a function as a “box containing a value” will tie your brain in knots. But for the sake of demystifying these concepts and building initial intuition, the “box” metaphor will do just fine. ↩︎