Monads, Applicatives & Functors

A no-BS guide that finally makes them click

Every time you see a value wrapped in some “context” — a list, a Maybe, a validation result, a parser, a configuration reader — you’re dealing with the same three problems:

Functor, Applicative and Monad are the exact tools that solve these three problems — in that order.

Functor – “Map inside the box”

flowchart LR A["f a"] -- "fmap f" --> B["f b"] style A fill:#4f46e5,stroke:#fff,color:#fff style B fill:#4f46e5,stroke:#fff,color:#fff

You have a plain function f :: a -> b and a value inside a context fa :: f a. How do you apply f without unwrapping?

fmap :: Functor f => (a -> b) -> f a -> f b

Real example: Updating a user record

data User = User
    { name :: String
    , age  :: Int
    , active :: Bool
    } deriving Show

-- We received a user that might be missing
maybeUser :: Maybe User
maybeUser = Just (User "Alice" 34 True)

-- Increment age, keep everything else
updated :: Maybe User
updated = fmap (\u -> u { age = age u + 1 }) maybeUser
-- => Just (User "Alice" 35 True)

Works the same for [User], Tree User, Parser User, etc. One function, any context.

Applicative – “Combine independent contexts”

flowchart TD A["pure (+)"] -- "<*>" --> B["Just 5"] B -- "<*>" --> C["Just 7"] C --> D["Just 12"] style A fill:#22c55e,stroke:#fff,color:#fff style B fill:#eab308,stroke:#fff,color:#111 style C fill:#eab308,stroke:#fff,color:#111 style D fill:#22c55e,stroke:#fff,color:#fff

You have a function that takes several arguments, and each argument lives in its own context. Applicative lets you apply the function to all of them at once.

pure  :: Applicative f => a -> f a
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Real example: Form validation that collects ALL errors

We want to validate a signup form and show every mistake at once (not stop at the first one).

import Data.Validation (Validation(..), _Failure, _Success)
import Data.Semigroup (Semigroup(..))

data FormError = EmptyField String | TooShort String | InvalidEmail
    deriving Show

validateName :: String -> Validation [FormError] String
validateName ""   = Failure [EmptyField "name"]
validateName n
    | length n < 3 = Failure [TooShort "name"]
    | otherwise     = Success n

validateEmail :: String -> Validation [FormError] String
validateEmail e
    | '@' `notElem` e = Failure [InvalidEmail]
    | otherwise       = Success e

data Signup = Signup { sName :: String, sEmail :: String }
    deriving Show

-- Applicative style: runs ALL validations
signup :: String -> String -> Validation [FormError] Signup
signup name email =
    Signup <$> validateName name <*> validateEmail email

-- Try it
main = do
    print $ signup "" "bad"          -- Failure [EmptyField "name", InvalidEmail]
    print $ signup "Bob" "[email protected]" -- Success (Signup "Bob" "[email protected]")

This is why Validation (from the validation package) is preferred over Either for forms — it’s Applicative and accumulates errors.

Monad – “Sequence dependent computations”

flowchart LR A["getUserById 42"] -- ">>=" --> B["\\user -> checkPassword user pw"] B -- ">>=" --> C["\\ok -> if ok then grantToken else deny"] style A fill:#8b5cf6,stroke:#fff,color:#fff style B fill:#8b5cf6,stroke:#fff,color:#fff style C fill:#8b5cf6,stroke:#fff,color:#fff

The next step depends on the result of the previous one. That’s exactly what >>= (bind) gives you.

(>>=) :: Monad m => m a -> (a -> m b) -> m b

Real example: Login flow (short-circuit on failure)

login :: String -> String -> IO (Either String Token)
login username password = runExceptT $ do
    user   <- ExceptT $ findUser username          -- Maybe -> Either
    _      <- ExceptT $ checkPassword user password -- Maybe -> Either
    token  <- liftIO $ generateToken user
    return token

Notice how each step can fail and we stop immediately — exactly what you want in a login flow.

The hierarchy (and when to use which)

Functor

Just transform the value inside.
Use when: one independent mapping.

Applicative

Combine several independent contexts.
Use when: validations, config merging, optional values.

Monad

Sequence dependent steps.
Use when: the next action depends on the previous result.

Every Monad is an Applicative.
Every Applicative is a Functor.
You should almost always start with Applicative and only go to Monad when you actually need dependency.