Zero‑Cost Domain Semantics with DerivingVia and Newtype Wrappers
Give primitives semantic meaning with zero‑cost wrappers using Haskell’s DerivingVia. Learn the pattern, mechanics, a worked example, trade‑offs, and how to adopt it safely in your codebase.
11 Dec 2025, 11:12 UTC

Concrete Problem: Meaningful Types Without Runtime Cost
In many Haskell codebases we wrap primitive values in newtype to give them semantic meaning – e.g. Meters instead of Double. The wrapper is great for type‑safety, but we still need instances for common type classes (Show, Eq, Num, ToJSON…) and writing them by hand quickly becomes boilerplate. The challenge is: how do we attach these instances to the newtype without adding runtime overhead or orphan instances?
Thesis: DerivingVia is the Zero‑Cost Solution
GHC’s DerivingVia extension lets a type derive an instance by delegating to another type specified via a via clause. When that other type is the underlying newtype’s field, the wrapper remains a pure compile‑time construct: the generated code performs the same operations as the underlying type, so there is no runtime penalty. In effect, DerivingVia gives us a clean, maintainable way to give domain semantics to primitives while reusing existing instances.
1. The Pattern in Action
The idiomatic pattern looks like this:
{-# LANGUAGE DerivingVia #-}
newtype Meters = Meters Double
deriving (Eq, Show, Num) via Double
Here Meters is a thin wrapper around Double; the via Double clause tells GHC to generate the Eq, Show, and Num instances by simply lifting the corresponding Double instances. The compiler rewrites the instance methods to unwrap the newtype, call the underlying Double method, and re‑wrap the result if necessary. Because newtype has no runtime representation, the generated code is identical to the Double implementation.
2. Mechanics of DerivingVia
- Syntax:
deriving (Class) via ViaType.ViaTypemust be a type that already has aClassinstance. - Constraints: The compiler checks that the
ViaType’s instance matches the target class’s method signatures. Mismatches surface as type‑checking errors, sometimes with cryptic messages if the class is complex. - Combining Strategies: You can mix
DerivingViawithDerivingStrategiesandGeneralizedNewtypeDerivingto selectively reuse instances while still deriving others in the “stock” fashion. - Zero‑Cost Guarantee: Because
newtypeis represented exactly like its field at runtime, the wrapper adds no allocation or indirection. The only cost is the small amount of code the compiler generates to perform the unwrap‑call‑wrap dance.
3. Worked Example: Domain Types for Time and Distance
Suppose we want two distinct units for time and distance, each with JSON serialization via Data.Aeson and a custom Unit class that provides a string label. Writing manual instances would be repetitive. Using DerivingVia keeps the code concise and zero‑cost.
{-# LANGUAGE DerivingVia, GeneralizedNewtypeDeriving #-}
import Data.Aeson (ToJSON, FromJSON, toJSON, parseJSON)
import Data.Aeson.Types (Parser)
-- Existing instance for Double
instance ToJSON Double where
toJSON = toJSON
instance FromJSON Double where
parseJSON = parseJSON
class Unit a where
unitLabel :: a -> String
instance Unit Double where
unitLabel _ = "generic"
-- Wrapper types
newtype Meters = Meters Double
deriving (Eq, Show, Num, ToJSON, FromJSON, Unit) via Double
newtype Seconds = Seconds Double
deriving (Eq, Show, Num, ToJSON, FromJSON, Unit) via Double
-- Usage
example :: IO ()
example = do
let distance = Meters 42
print (unitLabel distance) -- "generic"
print (toJSON distance) -- 42
print (distance + Meters 8) -- Meters 50
Notice that the Unit instance is also derived via Double. The compiler will generate a unitLabel method that simply forwards to Double's implementation. If you later want to give Meters a distinct label, you can override the instance manually while keeping other instances via Double:
instance Unit Meters where
unitLabel _ = "meters"
4. Trade‑Offs and Limitations
- GHC Version:
DerivingViawas added in GHC 8.6. Projects using older compilers must upgrade or use manual wrappers. - Instance Scope: If the
viatype comes from another module, the derived instance becomes an orphan unless the module exporting theviatype also declares the instance. This can lead to subtle compile‑time warnings. - Complex Classes: Classes with multi‑parameter type classes or functional dependencies may produce confusing error messages when the
viatype does not satisfy the constraints. Careful design of theviatype is important. - Performance of the Underlying Instance: DerivingVia does not hide expensive behaviour. If the underlying type’s instance is slow (e.g. a custom
Showthat formats thousands of digits), the wrapper will inherit that cost. - Debugging: Stack traces show the underlying type’s functions, not the wrapper, which may be confusing when debugging domain‑specific logic.
Practical Adoption Checklist
- Enable extensions in your module:
DerivingViaand, if needed,GeneralizedNewtypeDerivingandDerivingStrategies. - Define your
newtypewith a clear semantic name. - Use
deriving (Class) via Innerfor each class you want to reuse. Keep theviatype in the same module to avoid orphan instances. - Verify zero‑cost by compiling with
-ddump-simpland confirming that the wrapper’s code is inlined into the underlying type’s methods. - Add unit tests or QuickCheck properties that round‑trip values through the wrapper to ensure behavioural equivalence.
- Document the semantic meaning of each wrapper so future maintainers understand the domain intent.
By following this pattern, you gain type‑safety, clear domain semantics, and maintainable code without paying a runtime penalty. DerivingVia turns the tedious boilerplate of manual instances into a single, declarative line that the compiler can reason about efficiently.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.