Functors
Once you accept that Haskell separates data shape from computation, the Functor typeclass stops looking like academic ceremony and starts looking like the most reused interface in the language. fmap is everywhere. You will write it, see it in libraries, and hit type errors that mention it within your first week of real Haskell.
The idea is simple: there are many container-like types — Maybe, Either e, [], IO, Map k, parser combinators, futures from async — and applying a function to whatever they contain follows the same shape every time. Rather than write mapMaybe, mapEither, mapList, mapIO, Haskell unifies them under one operation.
The typeclass
class Functor f where
fmap :: (a -> b) -> f a -> f b
The infix synonym is <$>:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
f here is a type constructor of kind * -> *. That means Maybe (not Maybe Int), Either String (partially applied), [] (the list type constructor), IO. You cannot make Int a Functor — it has no slot to map over.
Concrete instances
Reading the instances is the fastest way to internalise what Functor means.
Maybe
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
If the value is missing, do nothing. If it is there, apply the function. The shape (Nothing vs Just) is preserved.
ghci> fmap (+1) (Just 3)
Just 4
ghci> (+1) <$> Nothing
Nothing
ghci> show <$> Just 42
Just "42"
Either
instance Functor (Either e) where
fmap _ (Left e) = Left e
fmap f (Right x) = Right (f x)
Notice Either e is partially applied. The Left channel is fixed; fmap only touches the Right. This is a deliberate choice — by convention Right is success, Left is the error, and you usually want to transform results, not errors.
ghci> fmap length (Right "hello" :: Either String String)
Right 5
ghci> fmap length (Left "boom" :: Either String String)
Left "boom"
Lists
instance Functor [] where
fmap = map
fmap on lists is map. That is it. The reason map exists at all is historical — it predates the Functor class.
IO
instance Functor IO where
fmap f action = do
x <- action
pure (f x)
fmap f getLine reads a line and applies f to the result, returning a new IO action. This is enormously useful:
upperLine :: IO String
upperLine = map toUpper <$> getLine
You will see this pattern constantly. <$> is how you transform a result without writing a do-block.
Why this abstraction matters
Before you have Functor, you would write things like:
applyToMaybe :: (a -> b) -> Maybe a -> Maybe b
applyToList :: (a -> b) -> [a] -> [b]
applyToTree :: (a -> b) -> Tree a -> Tree b
Three separate functions, three separate names to remember, three separate import paths. Functor collapses that into fmap.
But the real win is in code that is generic over the container. Here is a function that bumps a counter regardless of whether it lives in Maybe, a list, or an IO action:
bump :: Functor f => f Int -> f Int
bump = fmap (+1)
ghci> bump (Just 10)
Just 11
ghci> bump [1, 2, 3]
[2, 3, 4]
ghci> bump (pure 5 :: IO Int) >>= print
6
This pays off enormously in libraries. aeson returns parsed values inside Result or Parser, both Functors. The lens library leans on Functor in its core type. Streaming libraries like conduit and pipes all expose Functor instances. Once you know fmap, you know how to transform a value coming out of any of them.
The laws
Every Functor instance must satisfy two laws. The compiler does not check these — you do, by writing tests or by inspection.
Identity
fmap id = id
Mapping the identity function over a structure does nothing. If you broke this, fmap id (Just 5) could return something other than Just 5, and the whole abstraction would lie.
Composition
fmap (g . f) = fmap g . fmap f
Mapping a composed function is the same as mapping each piece in turn. This means GHC can fuse fmap f . fmap g into one traversal — and it does, when the rules fire. If your custom Functor breaks this, your users will be confused when refactoring breaks code.
A "Functor" that is not lawful is sometimes called a "fake Functor" — the type signature is right, but the behaviour does not match what library authors expect. An example violation:
-- DON'T do this
data Counted a = Counted Int a
instance Functor Counted where
fmap f (Counted n x) = Counted (n + 1) (f x)
This breaks identity: fmap id (Counted 0 x) = Counted 1 x, not Counted 0 x. Anything generic over Functor that assumed lawfulness will silently misbehave.
Functor over functions
A surprising one: functions themselves are Functors.
instance Functor ((->) r) where
fmap = (.)
fmap for (->) r is just function composition. If you have a function r -> a and want r -> b, you compose with a -> b:
ghci> fmap (+1) (*2) $ 10
21 -- (10 * 2) + 1
This is rarely useful day-to-day but shows up in lens combinators and effect libraries.
Common patterns in real code
Stripping a result out of an IO and transforming it:
import Data.Time
todayWeekday :: IO String
todayWeekday = show . dayOfWeek . utctDay <$> getCurrentTime
Parsing JSON and reshaping the result:
import Data.Aeson
import qualified Data.ByteString.Lazy as BL
parseUser :: BL.ByteString -> Maybe Text
parseUser bs = userName <$> decode bs
Mapping over the right side of an Either returned from a database call (in libraries like persistent or hasql):
fetchAge :: UserId -> IO (Either DbError Int)
fetchAge uid = fmap userAge <$> getUser uid
-- The outer fmap is over IO, the inner over Either DbError.
That last example is worth pausing on. Two Functors stacked: IO (Either DbError User). The outer fmap (or <$>) lifts into IO. The inner fmap lifts into Either DbError. To go two levels deep you compose them: fmap . fmap.
fetchAge uid = (fmap . fmap) userAge (getUser uid)
This trick is common when you have m (f a) and want to touch the a.
Common pitfalls
Confusing fmap with map. They are the same on lists, but fmap is more general. New Haskellers often write map everywhere out of habit and then get stuck when they need it on Maybe. Default to <$>; it works on lists too.
Trying to combine results from two Functors with fmap. fmap takes a single-argument function. If you have Just 3 and Just 4 and want Just 7, fmap cannot get you there alone. That is what Applicative is for.
Writing unlawful instances. If your "container" needs to record metadata that changes when mapped, that metadata is part of the value, not the structure. Either fold it into the value or use a different abstraction.
Forgetting that fmap can change the inner type but not the outer. fmap show (Just 5) :: Maybe String. The Maybe stays. You cannot use fmap to turn Maybe Int into Either String Int.
Stacking too many fmaps without thinking. If you find yourself writing fmap . fmap . fmap, consider whether traverse, a transformer, or just a do-block would read better.
Key takeaways
- Functor abstracts "apply a function inside a structure" across
Maybe,Either e,[],IO, parsers, futures, and many more. fmap :: (a -> b) -> f a -> f b; the infix<$>is what you read in real code.- Two laws:
fmap id = idandfmap (g . f) = fmap g . fmap f. Break them and you break everyone's intuition. fmaponEither eworks onRight, leaving errors alone. This convention runs through the whole ecosystem.- For nested structures, compose:
(fmap . fmap)reaches one level deeper. - Functor is the floor, not the ceiling. When you need to combine multiple wrapped values, you reach for Applicative or Monad.