Cutting Boilerplate with GHC's DerivingVia: Composable Typeclass Instances Without Newtype Wrappers
GHC's DerivingVia extension lets you attach typeclass instances by delegating to existing instances — no newtype wrappers, zero runtime cost. A practical walkthrough with Age/Sum Int, chaining strategies, and Core verification.
20 Aug 2025, 18:49 UTC

The Problem: Newtype Wrappers for Every Semantic Variation
Haskell developers routinely wrap primitive types in newtype to give them distinct typeclass semantics — Sum Int for additive monoids, Product Int for multiplicative ones, Down a for reverse ordering. Each wrapper demands a separate type definition and often a manual instance declaration, even when the instance is mechanically derived from the wrapped type. This boilerplate obscures intent and clutters modules.
DerivingVia: Reuse Instances at the Representation Level
Since GHC 8.6 (2018), the DerivingVia extension lets you attach a typeclass instance to a datatype by delegating to an existing instance for another type — without defining a newtype. The compiler reuses the dictionary of the via type at the representation level, so there is zero runtime overhead. You write the delegation directly in the data declaration, keeping the semantics visible where the type is defined.
Worked Example: Additive Semantics for a Domain Type
Suppose you model ages as a distinct type but want Semigroup and Monoid instances that add ages (useful for combining age ranges). With DerivingVia you avoid a newtype Age = Age Int plus a hand-written Monoid instance.
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
newtype Age = Age Int
deriving stock (Eq, Show)
deriving via (Sum Int) instance Semigroup Age
deriving via (Sum Int) instance Monoid Age
Each deriving via clause names the via type (Sum Int) and the class. GHC checks that Age and Sum Int have the same runtime representation (they do, both are single Int fields) and copies the Semigroup and Monoid dictionaries. In GHCi you can verify:
> :set -XDerivingVia -XGeneralizedNewtypeDeriving -XDerivingStrategies
> newtype Age = Age Int deriving stock (Eq, Show) deriving via (Sum Int) instance Semigroup Age; deriving via (Sum Int) instance Monoid Age
> Age 5 <> Age 3
Age 8
> mempty :: Age
Age 0
Run this in a GHCi session (GHC ≥ 8.6) after enabling the three language extensions. No special permissions needed. The :info Age command will list the derived instances.
Chaining Different Semantics on One Datatype
Because each class gets its own deriving via clause, you can mix semantics. A type representing a price could have additive Semigroup (for summing line items) but multiplicative Monoid (for compounding discounts) by delegating to Sum Int and Product Int respectively. The declarations stay together, making the design explicit.
Integration with Other Deriving Strategies
DerivingVia coexists with stock (built-in), newtype (GeneralizedNewtypeDeriving), and anyclass. You can pick per-class:
data Config = Config Int
deriving stock Eq
deriving newtype Show
deriving via (Down Int) instance Ord Config
Here Ord uses reverse ordering via Down Int, while Eq and Show use the default strategies. The deriving keyword appears once per strategy group, keeping the syntax regular.
Trade-offs and Limitations
- Coherence is on you. GHC cannot prove that two different
deriving viapaths to the same class produce extensionally equal dictionaries. If you accidentally deriveMonoidvia bothSum IntandProduct Intfor the same type in different modules, you break coherence — the compiler trusts you. - Type inference may need help. When the via type has ambiguous parameters (e.g.,
Const m a), you may need visible type applications:deriving via (Const m a @m @a) instance Monoid m => Monoid (MyWrapper a). - Readability for newcomers. Overuse hides which instance is actually in scope. For domain-specific semantics that aren't standard (e.g., a custom monoid for validation errors), an explicit
newtypewith a hand-written instance is clearer.
Verifying Zero-Cost Abstraction
To confirm no wrapper allocation occurs, compile with -ddump-simpl and inspect the Core. The derived methods should be direct calls to the via type's methods. Example command (run in project root with a .cabal or package.yaml that enables DerivingVia):
ghc -O2 -ddump-simpl -dsuppress-all Age.hs | grep -A5 "$w<>"
You should see a call to Sum.$fSemigroupSum_$s<> or similar, not a constructor wrap/unwrap pair. This check requires a GHC installation (any recent version) and read access to the source file.
When to Reach for DerivingVia
Use DerivingVia when the semantics you want already exist as an instance for a representation-compatible type — standard monoids, Down for ordering, ReaderT for mtl-style constraints — and you want to keep the declaration site self-documenting. Reserve explicit newtype + instance for novel semantics or when the delegation would puzzle a maintainer unfamiliar with the via type.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.