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.
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
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.
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
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.
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
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.
Just transform the value inside.
Use when: one independent mapping.
Combine several independent contexts.
Use when: validations, config merging, optional values.
Sequence dependent steps.
Use when: the next action depends on the previous result.