blog :: Post "haskell-deriving-strategies"
Deriving Strategies in Haskell
2026-08-09 · 7 min
A deriving clause in Haskell can mean four different things, and for most of the language's history there was no way to say which one you meant. Deriving strategies make the choice explicit. This is what each one generates and when it is the right answer.
A little history
Haskell 98 had one kind of deriving, for a fixed list of classes the compiler knew how to write instances for: Eq, Ord, Show, and a handful more. GeneralizedNewtypeDeriving arrived to let a newtype borrow its underlying type's instances. DeriveAnyClass arrived to generate an empty instance so a class's default methods apply. Both attach to the same deriving keyword, so once both were enabled the meaning of a clause depended on a resolution order most people could not recite. DerivingStrategies (GHC 8.2) added the keywords to disambiguate, and DerivingVia (GHC 8.6) added a fourth strategy that generalizes the newtype one.
None of the four keywords are in GHC2021, so you enable them per module or in the .cabal file. StandaloneDeriving and GeneralizedNewtypeDeriving are in GHC2021 already.
{-# LANGUAGE DerivingStrategies #-}{-# LANGUAGE DeriveAnyClass #-}{-# LANGUAGE DerivingVia #-}
Turn on the warning that flags any deriving clause without an explicit strategy. This is the setting that makes the rest of the post enforceable rather than advisory.
ghc-options: -Wmissing-deriving-strategies
stock
The compiler writes the instance itself, using knowledge built into GHC about that specific class. This is the Haskell 98 behaviour, plus the classes unlocked by the Derive* extensions: Functor, Foldable, Traversable, Generic, Data, Lift.
Structural instances, generated from the shape of the data type.
data Config = Config{ host :: String, port :: Int}deriving stock (Eq, Show, Generic)
What you get: field-by-field comparison, and a Show that round-trips through Read.
ghci> show (Config "localhost" 8080)Config {host = "localhost", port = 8080}
The list is fixed. You cannot derive stock for a class of your own, no matter how mechanical the instance would be. That restriction is what the other three strategies exist to work around.
newtype
A newtype has the same runtime representation as the type it wraps, so any instance the underlying type has can be reused by coercion, at no cost. This is the strategy for wrappers that exist to add type safety rather than behaviour.
UserId is an Int at runtime. It gets Int's arithmetic and ordering, coerced.
newtype UserId = UserId Intderiving newtype (Eq, Ord, Num, Hashable)
The distinction that matters. stock Show prints the constructor; newtype Show prints what the underlying type would.
newtype UserId = UserId Int deriving stock (Show)UserId 42newtype UserId = UserId Int deriving newtype (Show)42
Mixing strategies in one declaration is allowed, and usually what you want: structural equality and a constructor-revealing Show, but the underlying numeric behaviour.
newtype Meters = Meters Doublederiving stock (Show)deriving newtype (Eq, Ord, Num, Fractional)
The limit is coercibility. GHC has to be able to convert between the class's methods at the two types, which fails when the type variable appears somewhere the role system will not allow, such as inside a type family or a Data.Set whose ordering would change.
anyclass
Generates an empty instance body, so every method falls back to the class's default implementation. This is only useful for classes whose defaults actually do something, which in practice means classes with Generic-based defaults.
aeson's ToJSON and FromJSON have generic defaults, so an empty instance is a complete one. Generic itself is stock-derived.
data Config = Config{ host :: String, port :: Int}deriving stock (Generic)deriving anyclass (ToJSON, FromJSON)
The equivalent written by hand. anyclass is writing exactly this.
instance ToJSON Configinstance FromJSON Config
The trap. Eq's default (==) is defined as not . (/=), and its default (/=) as not . (==). An empty instance means both, and any comparison hangs.
newtype UserId = UserId Intderiving anyclass (Eq) -- compiles, then loops forever
Nothing warns you about that, because an empty instance is exactly what you asked for. The rule worth keeping: reach for anyclass only when you know the class has defaults that stand on their own, and never for a class that also appears in the stock list.
via
The newtype strategy coerces through the type immediately underneath. via lets you name any type with the same representation and borrow its instance instead. That turns "which behaviour do I want" into a type you write down, rather than a hand-written instance.
Int has several reasonable monoids. Naming the one you mean is the whole feature.
newtype Score = Score Intderiving stock (Show, Eq)deriving (Semigroup, Monoid) via (Sum Int)newtype HighScore = HighScore Intderiving stock (Show, Eq)deriving (Semigroup, Monoid) via (Max Int)
The via type does not have to be a newtype of your type. It has to be representationally equal, and to have the instance you want.
ghci> foldMap Score [1, 2, 3]Score 6ghci> foldMap HighScore [1, 2, 3]HighScore 3
Where via earns its keep: encoding configuration that would otherwise be a hand-written instance. deriving-aeson carries the field-naming rules in the type.
data User = User{ userName :: String, userEmail :: String}deriving stock (Generic)deriving (ToJSON, FromJSON)via CustomJSON'[ FieldLabelModifier (StripPrefix "user", CamelToSnake) ]User
The result, without an instance body anywhere in the module.
ghci> encode (User "ada" "ada@example.com"){"name":"ada","email":"ada@example.com"}
You can also write the via target yourself, which is how a team shares one convention across many types.
newtype AsMillis a = AsMillis ainstance ToJSON (AsMillis NominalDiffTime) wheretoJSON (AsMillis t) = toJSON (round (t * 1000) :: Int)newtype Timeout = Timeout NominalDiffTimederiving ToJSON via (AsMillis NominalDiffTime)
Standalone deriving
A strategy can also be applied in a standalone clause, away from the data declaration. You need this when the instance requires a context the inline form cannot express, when the type is defined in another module, or when the via type needs to mention a type variable.
The keyword goes between deriving and instance.
deriving stock instance Show a => Show (Tree a)deriving newtype instance Num Metersderiving anyclass instance ToJSON Config
A context the inline clause would have inferred wrongly, or refused.
data Rose a = Rose a [Rose a]deriving stock instance Eq a => Eq (Rose a)deriving stock instance Ord a => Ord (Rose a)
Which one
The decision, in the order worth asking it.
Is it Eq/Ord/Show/Read/Enum/Bounded/Ix/Functor/Foldable/Traversable/Generic/Data/Lift?-> stockIs it a newtype that should behave exactly like itsunderlying type?-> newtypeDo you want behaviour that some other type already has,or that you can name in a type?-> viaDoes the class have Generic-based defaults that arecomplete on their own?-> anyclass
Check what a clause actually generated when the answer is not obvious.
$ ghc -ddump-deriv -dsuppress-all Config.hs
The short version: turn on -Wmissing-deriving-strategies, write the keyword every time, use newtype for wrappers that should disappear at runtime, via when the behaviour you want already exists somewhere with a name, and anyclass only for classes whose defaults are the real implementation.