Functor, Applicative, and Monad can sound like a barrier to functional programming. They are not three competing ideas or magical containers. They are progressively more capable interfaces for working with a value while preserving the structure around it.
In PureScript, that structure might represent an optional value (Maybe a), a computation that can return an error (Either e a), an array of possibilities, or an effectful program (Effect a). The useful question is not “what metaphor fits?” but: what can I do without manually taking the value out and rebuilding its context?
Start with the type, not the metaphor
Maybe a says a value of type a may be absent. Either e a says a computation produces either an error e or a successful value a. Effect a describes a program which, when run, may perform synchronous JavaScript effects and produce a; asynchronous work is conventionally represented with Aff a.
These types are not runtime security or validation by themselves. Data from HTTP, a database, or JavaScript is still untrusted until validated at the boundary. The abstractions below help us compose the resulting typed values cleanly.
Functor: transform a value while keeping its context
A Functor supports map (also written <$>). Given a normal function a -> b, it transforms the value inside f a into f b.
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
For Maybe, mapping runs the function for Just and preserves Nothing.
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (logShow)
toCents :: Int -> Int
toCents dollars = dollars * 100
main :: Effect Unit
main = do
let amount = Just 50 :: Maybe Int
let missing = Nothing :: Maybe Int
logShow (toCents <$> amount) -- Just 5000
logShow (toCents <$> missing) -- Nothing
toCents stays a simple Int -> Int function. The Maybe Functor owns the “what happens when the value is missing?” rule. Use a Functor whenever the next operation is a pure transformation of one available value.
Applicative: combine independent contextual values
An Applicative extends Functor with pure, which places a value in a context, and apply (written <*>), which applies a contextual function to a contextual value.
class Functor f <= Applicative f where
pure :: forall a. a -> f a
apply :: forall a b. f (a -> b) -> f a -> f b
This is especially readable when a constructor needs several independently obtained values.
import Prelude
import Data.Maybe (Maybe(..))
type User = { name :: String, id :: Int }
makeUser :: String -> Int -> User
makeUser name id = { name, id }
maybeName :: Maybe String
maybeName = Just "Alice"
maybeId :: Maybe Int
maybeId = Just 1024
maybeUser :: Maybe User
maybeUser = makeUser <$> maybeName <*> maybeId
-- Just { name: "Alice", id: 1024 }
If either input is Nothing, the result is Nothing. Notice the function itself remains pure; Applicative handles the shared Maybe context. pure 42 :: Maybe Int produces Just 42.
“Independent” matters. Applicative composition fits when the next computation does not need the previous value to decide what computation to run. For validation that should collect several errors, use a validation type with an error-accumulating Applicative instance; Either normally returns one failure rather than collecting all of them.
Monad: choose the next computation from the previous result
A Monad adds bind (often written >>=). It lets the next function return another contextual value:
class Applicative m <= Monad m where
bind :: forall a b. m a -> (a -> m b) -> m b
This is needed when a plain map would create nesting such as Maybe (Maybe Int), or when later work depends on an earlier successful result.
import Prelude
import Data.Either (Either(..))
import Data.Int (fromString)
import Data.Maybe (note)
parsePositiveInt :: String -> Either String Int
parsePositiveInt raw = do
value <- note "Expected an integer" (fromString raw)
if value > 0 then Right value
else Left "Expected a positive integer"
orderTotal :: String -> String -> Either String Int
orderTotal rawPrice rawQuantity = do
price <- parsePositiveInt rawPrice
quantity <- parsePositiveInt rawQuantity
pure (price * quantity)
Here, each <- extracts a successful Right for the next line. If parsePositiveInt returns Left, the remaining work is skipped and that error becomes the result. That short-circuiting is behavior of the Either Monad instance not a universal property of every Monad.
do notation is readable bind
PureScript’s do notation makes sequential composition practical. Conceptually, this:
do
price <- parsePositiveInt rawPrice
quantity <- parsePositiveInt rawQuantity
pure (price * quantity)
is a readable form of:
parsePositiveInt rawPrice >>= \price ->
parsePositiveInt rawQuantity >>= \quantity ->
pure (price * quantity)
Use do for effectful programs too. The same sequencing idea works for Effect, Aff, Maybe, and Either; only the meaning of sequencing changes with the instance.
A practical selection guide
- Functor: transform one value with a pure function. Example: format a
Maybe Date. - Applicative: combine contextual values when their computations are independent. Example: construct a record from optional fields.
- Monad: continue with a computation selected by an earlier result. Example: parse an identifier, then load data using that identifier.
Start with the least powerful abstraction that expresses the flow. <$> is often clearer than do for one transformation; <*> clearly communicates independent inputs; use do when sequencing or dependency is genuinely present.
Common mistakes
- Calling every generic type constructor a “container.” It is a helpful first picture, not the definition.
- Assuming types make untrusted input valid. Decode and validate runtime data before relying on static types.
- Using Monad when Applicative expresses the intent better.
- Assuming every Monad stops on failure. Arrays, state, readers, and effects have different sequencing behavior.
- Confusing
Effectwith asynchronous work. UseAffwhen the operation is asynchronous.
Conclusion
Functor, Applicative, and Monad are less mysterious when treated as composition tools. Functor transforms within a context. Applicative combines independent contextual values. Monad sequences computations whose next step depends on the previous result.
Once these distinctions become familiar, types like Maybe, Either, Effect, and Aff stop feeling like ceremony. They become explicit descriptions of how values and computations should flow through the program.

Top comments (0)