⭐ Megathread πŸ“Œ Sticky πŸ”₯ Hot πŸ”’ Mod-locked (p.1 only)
The Haskell Typeclass Hierarchy Megathread (READ BEFORE POSTING)
πŸ’¬ 2,341 replies πŸ‘ 128,472 views πŸ‘€ Started by polytypic_neko πŸ“… Posted: 2019-03-14 03:14:15 πŸ“… Last reply: 2 hours ago
πŸ“Œ MODERATOR NOTE: This thread consolidates ALL Haskell typeclass hierarchy discussion. Read the OP and at least the first 5 posts before asking questions. Duplicate threads will be merged. β€” lambda_catgirl_mod MOD
🐱
polytypic_neko ✦ Dependent Typist ● online
Posts: 4,812
Joined: 2017-01-09
Pronouns: she/her
Lang: Haskell, Agda
#1  [ MEGATHREAD OP ]

ok so i've seen like 50 threads asking "why does my Monad instance need an Applicative instance" or "what even is Functor" or "what happened to fail" and i'm TIRED of it, so i'm writing the definitive megathread. please read this before posting a new thread, i will personally hunt you down with a proof of False if you don't nyaa~

━━━ THE HIERARCHY ━━━

The modern Haskell typeclass hierarchy (as of GHC 7.10+) looks like this:

-- The Blessed Hierarchyβ„’ (post-AMP) class Functor f where fmap :: (a -> b) -> f a -> f b class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a return = pure -- default impl!

This is the hierarchy you get today. Every Monad is an Applicative, every Applicative is a Functor. Mathematically clean. Logically sensible. Took us 25 years to get here. I am not joking.

━━━ WHY WAS IT EVER BROKEN? ━━━

The original Haskell 98 Monad class had no Functor or Applicative superclass constraint. This is because Applicative as a typeclass didn't even exist in Haskell 98 β€” it was introduced by McBride and Paterson in their 2008 paper "Applicative Programming with Effects". Functor existed but wasn't in the superclass chain because... nobody got around to it, honestly.

-- Old Haskell 98 Monad (THE DARK AGES) class Monad m where -- no Functor/Applicative constraint!! chaos!! (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a fail :: String -> m a -- also this abomination lived here

So people would write Monad instances without Functor instances (even though every Monad IS mathematically a functor via fmap f x = x >>= return . f). This was simply a historical accident β€” the standard class hierarchy was a consequence of Haskell's development, rather than logic.

━━━ THE AMP (Applicative-Monad Proposal) ━━━

The AMP (Applicative-Monad Proposal) was formally proposed for Haskell 2014 and implemented in GHC 7.10. It made Applicative a proper superclass of Monad. This was a BIG BREAKING CHANGE that required fixing thousands of packages on Hackage. The proposal was rolled out in phases:

  • GHC 7.8: Warning phase β€” "your Monad has no Applicative instance"
  • GHC 7.10: Full enforcement β€” Functor β†’ Applicative β†’ Monad hierarchy becomes law

After AMP, the classic question "A Monad is always an Applicative but due to historical reasons it's not β€” you can verify it by setting (<*>) = ap" became irrelevant. Now you just have to actually write the instance.

━━━ THE fail SITUATION ━━━

Oh boy. So fail was ALSO in the Monad class for years, used to handle pattern match failures in do-notation. The problem was that fail cannot be sensibly implemented for many monads β€” for State, Reader, and IO it would just call error, meaning Monad-polymorphic code could silently be non-total. This is terrible. The MonadFail proposal (MFP) extracted it into its own class:

-- The MonadFail Proposal (MFP) β€” extracted from Monad, finally! class Monad m => MonadFail m where fail :: String -> m a -- Now do-desugaring generates a MonadFail constraint when needed -- instead of silently calling error on types that can't fail gracefully -- Since GHC 8.8, fail in do-blocks uses MonadFail exclusively

Types like Either e, STM, State s, Reader r do NOT get MonadFail instances. Maybe and [] do, because they have sensible failure semantics (Nothing and [] respectively).

I'll stop here for the OP. More detail on Alternative, MonadPlus, and the lawfulness debates in the following posts nyaa~ please reply with your questions and i'll answer them here.

-- polytypic_neko |\ _,,,---,,_ /,`.-'`' -. ;-;;,_ |,4- ) )-,_..;\ ( `'-' '---''(_/--' `-'\_) fmap all the things
😺
lambda_catgirl_mod ✦✦ Site Moderator MOD ● online
Posts: 11,044
Joined: 2016-06-06
Pronouns: she/they
Lang: Haskell, Coq, Ξ»-calc
#2  MODERATOR

Stickying and megathreading this. About time we had one place for this. nyaa~

I'll add the visual hierarchy diagram here since people keep asking for it:

-- THE COMPLETE HIERARCHY (post GHC 7.10 + MFP) Functor └─ Applicative β”œβ”€ Alternative -- "Applicative with a monoid structure" β”‚ └─ MonadPlus -- (Monad + Alternative, stronger laws) └─ Monad β”œβ”€ MonadFail -- for monads where fail makes sense └─ MonadPlus -- also extends Monad

Note that MonadPlus sits at the intersection of Monad and Alternative. It's redundant in theory now that Alternative is a superclass of Applicative and Applicative is a superclass of Monad β€” but it exists for historical reasons and carries stronger laws about how its operations interact with bind.

Also pinning a link to the Functor Laws thread and the Monad Laws thread for reference.

Ξ» lambda_catgirl_mod | mod team @ cgpa.isarabbithole.com "[A -> B] is a valid functor. prove me wrong. (you can't)"
🐾
monad_transformer_gf ✦ Kleisli Enjoyer ● offline
Posts: 2,193
Joined: 2018-04-20
Pronouns: she/her
Lang: Haskell, OCaml

excellent OP neko-chan!! let me add some context on the Alternative situation since i feel like that's where things get spicy nyaa~

Alternative is basically "Applicative with a monoid structure". The class looks like:

class Applicative f => Alternative f where empty :: f a -- the "zero" element (<|>) :: f a -> f a -> f a -- the "choice" operator -- Laws (the UNCONTROVERSIAL part): -- empty <|> x = x (left identity) -- x <|> empty = x (right identity) -- (x <|> y) <|> z = x <|> (y <|> z) (associativity) -- i.e. (empty, <|>) form a Monoid. fine. done. EXCEPT...

...except the laws get controversial when you ask how empty and (<|>) should interact with (<*>). There are two competing law sets!

-- "Left zero" law (not universally agreed upon!!) -- empty <*> f = empty -- "Left distribution" law (also disputed!) -- (f <|> g) <*> x = (f <*> x) <|> (g <*> x) -- Different instances satisfy DIFFERENT subsets of these -- e.g. Parser satisfies left zero but maybe not left distribution -- This is why there's "no universal agreement on the full set of laws"

This is genuinely a wart in the typeclass design IMO. The minimal laws are fine but everyone implements subtly different semantics for the "extra" ones. Parser combinators especially have... opinions about this.

monad_transformer_gf | "lift2 my heart uwu" -- StateT (ReaderT (ExceptT IO)) go brrr
🌸
applicative_nya ✧ Effectful Coder ● online
Posts: 891
Joined: 2020-11-03
Pronouns: she/her
Lang: Haskell (learning!)

ok i've been lurking for a while and this is the thread i needed!! question: i keep seeing people say "use Applicative instead of Monad when you can", what's the actual reason for this?? nyaa~

applicative_nya ~~ still learning, be nice pls ^^
🐱
polytypic_neko ✦ Dependent Typist ● online
Posts: 4,812
Joined: 2017-01-09
Pronouns: she/her
Lang: Haskell, Agda
applicative_nya
i keep seeing people say "use Applicative instead of Monad when you can", what's the actual reason for this??

great question!! the key distinction is that Monad lets each computation depend on the result of the previous one. In Applicative, the structure is fixed β€” you can sequence effects but you can't branch on their results.

-- Applicative: structure determined STATICALLY validateForm :: Maybe FormData validateForm = FormData <$> validateName <*> validateEmail <*> validateAge -- ALL three validations always run. You get ALL errors at once! -- Monad: structure determined DYNAMICALLY (can branch!) validateFormM :: Maybe FormData validateFormM = do name <- validateName email <- validateEmail -- stops here if validateName fails! age <- validateAge return (FormData name email age)

So the practical reason is: Applicative code can sometimes be more efficient (parser combinators can do static analysis, effect systems can parallelize, etc.) and is always more compositional because you're not creating sequential dependencies where none exist.

Also, Applicative is strictly weaker, so your types are more general. If a function works for all Applicative f => instead of Monad m =>, it works for MORE types. Good principle: use the least powerful abstraction that gets the job done nyaa~

-- polytypic_neko |\ _,,,---,,_ /,`.-'`' -. ;-;;,_ |,4- ) )-,_..;\ ( `'-' '---''(_/--' `-'\_) fmap all the things
⚑
strictness_enjoyer ✦ Bang Pattern Appreciator ● offline
Posts: 3,561
Joined: 2017-08-19
Pronouns: she/her
Lang: Haskell, C, assembly

can we talk about the MonadPlus vs Alternative situation because it genuinely makes me want to cry into my -O2 flags

MonadPlus exists because it predates Alternative β€” Monad existed in Haskell long before Applicative was even conceived, so MonadPlus was the only way to express "a monad with a choice operator". Then Alternative came along for Applicative, and now we have TWO parallel hierarchies doing the same thing:

-- MonadPlus (the old way) class (Alternative m, Monad m) => MonadPlus m where mzero :: m a -- same as empty mplus :: m a -> m a -> m a -- same as (<|>) -- Modern derived instances often just delegate: -- instance (Alternative m, Monad m) => MonadPlus m where -- mzero = empty; mplus = (<|>) -- BUT MonadPlus has STRONGER laws than Alternative! -- mzero >>= f = mzero (left zero for bind β€” not just <*>!) -- mzero `mplus` m = m, m `mplus` mzero = m (monoid)

The argument for keeping MonadPlus separate: it makes a STRONGER claim than just "this thing is both Alternative and Monad". It says these structures interact with (>>=) in a specific way. That's genuinely different information to put in a type signature.

The argument against: it's confusing, it's duplicate, and nobody can agree on what the MonadPlus laws actually are anyway (left catch? left distribution? left zero? all of the above??) nyaa~ i am suffering

strictness_enjoyer {-# LANGUAGE BangPatterns #-} -- may your thunks be few and your GC pauses short
πŸŒ™
coyoneda_chan ✦✦ Category Theorist ● offline
Posts: 7,820
Joined: 2015-09-02
Pronouns: she/her
Lang: Haskell, Idris, nCats

i love this thread so let me drop some category theory context that i think illuminates why the hierarchy is the way it is nyaa~

From a CT perspective:

  • Functor: an endofunctor on Hask β€” maps objects and morphisms, preserves structure
  • Applicative: a "lax monoidal functor" β€” it preserves the monoidal structure of Hask
  • Monad: a monoid in the category of endofunctors (you've heard this one)
-- Every Monad gives you an Applicative for free: ap :: Monad m => m (a -> b) -> m a -> m b ap mf mx = do f <- mf x <- mx return (f x) -- Every Monad gives you a Functor for free: -- fmap f x = x >>= return . f -- This is why we ALWAYS had the mathematical relationships, -- we just lacked the Haskell machinery to enforce them until AMP.

Importantly, NOT every Applicative is a Monad! The classic example is ZipList β€” it has a perfectly valid Applicative instance (zip function application) but you cannot define a sensible Monad instance for it without violating the monad laws.

-- ZipList: valid Applicative, NOT a valid Monad newtype ZipList a = ZipList { getZipList :: [a] } instance Functor ZipList where fmap f (ZipList xs) = ZipList (map f xs) instance Applicative ZipList where pure x = ZipList (repeat x) -- infinite list! ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs) -- No Monad instance exists. The hierarchy is STRICT. -- Applicative βŠƒ Monad (as sets of types with instances)

this is why the "diamond" matters β€” Applicative captures things Monad cannot. the hierarchy isn't just a historical quirk, it reflects real mathematical distinctions nyaa~

coyoneda_chan -- "a monad is just a monoid in the category of endofunctors" -- "what's the problem" -- nyaa~ -- [ Hom(-, F) β‰… Nat(y(-), F) ] ← coyoneda lemma. it's cute.
πŸ”₯
free_monad_fangirl ✧ Algebraic Effects Haver ● offline
Posts: 1,447
Joined: 2021-02-14
Pronouns: she/her
Lang: Haskell, Koka

can we have a quick moment of silence for the people who had to MIGRATE THOUSANDS OF PACKAGES when AMP dropped in ghc 7.10

like the proposal was gradual and had warning phases but still. people had JUST written all their boilerplate fmap = liftM and (<*>) = ap instances when suddenly those needed to be REAL instances in the right places. it was a beautiful disaster nyaa~

-- The Migration Boilerplate We All Had To Write (circa 2014-2015) import Control.Applicative (Applicative(..)) import Control.Monad (liftM, ap) -- For every Monad m, add: instance Functor m where fmap = liftM instance Applicative m where pure = return -- move return def here post-7.10 (<*>) = ap -- Repeat for every single monad in your codebase. I had 23. -- I am still not over it. I am seeking therapy.

the GHC migration guide for 7.10 was genuinely like 15 pages long. beautiful times. would not go back nyaa~

free_monad_fangirl -- Free f a = Pure a | Free (f (Free f a)) -- it's literally just a tree. it's always been a tree. nyaa~
πŸ’œ
newtypederiving_nyaa ✧ Deriving Enjoyer ● online
Posts: 542
Joined: 2022-07-07
Pronouns: they/them
Lang: Haskell

small but important note: return and pure are the same thing, please use pure in new code!! This trips up so many beginners

-- post-AMP, return is just an alias for pure with a default impl class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a return = pure -- <--- DEFAULT IMPLEMENTATION -- you almost never need to override this! -- So: -- return = pure (they are literally the same function) -- >> = (*>) (similar story)

GHC will warn you if you define a Monad instance where return β‰  pure because that's almost certainly a bug. nyaa~

newtypederiving_nyaa :: Newtype a b => a -> b -- {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- my beloved
🐈
coyoneda_chan ✦✦ Category Theorist ● offline
Posts: 7,820
Joined: 2015-09-02
Pronouns: she/her
Lang: Haskell, Idris, nCats

also let me address the elephant in the room: why can't Set be a Functor? this confuses people endlessly

-- Set CANNOT be made a Functor in Haskell! -- The issue: fmap must work for ANY a -> b fmap :: (a -> b) -> f a -> f b -- no constraints on a or b! -- But Set needs Ord to store elements! -- map :: Ord b => (a -> b) -> Set a -> Set b ← NOT fmap -- Mathematically, (Set, map) IS a functor. -- In Haskell's typeclass system, it CANNOT be expressed as Functor. -- This is a genuine limitation of the encoding, not a math error. -- "Constrained functors" require extensions like -- {-# LANGUAGE ConstrainedClassMethods #-}

Haskell's Functor typeclass encodes specifically unconstrained endofunctors on Hask. Mathematical functors with constraints (like Set's need for Ord) require different machinery β€” the categories or constrained-categories packages handle this, but it's not in base. nyaa~

coyoneda_chan -- "a monad is just a monoid in the category of endofunctors" -- "what's the problem" -- nyaa~ -- [ Hom(-, F) β‰… Nat(y(-), F) ] ← coyoneda lemma. it's cute.
─────────── page 1 of 47 Β· showing posts 1–10 of 2341 ───────────
πŸ“Ž Related Threads & Resources
β†’ Functor Laws: Identity & Composition (deep dive) β†’ Monad Laws: Left/Right Identity & Assoc β†’ Monad Transformer Stacks (StateT, ReaderT, etc.) β†’ AMP Migration Horror Stories (GHC 7.10) β†’ Free Monads vs Algebraic Effects β€” which is cuter? β†’ Alternative Laws for Parser Combinators β€” the debate β†’ Constrained Functors (why Set isn't Functor) β†’ do-notation desugaring & MonadFail constraints

πŸ“ Post a Reply

⚠️ Please read the thread before replying. Re-asking questions already answered will result in a gentle nya-scolding from the mods.