Using Haskell’s DerivingVia to Flip Ordering with Data.Ord.Down
Flip numeric ordering in Haskell with DerivingVia and Data.Ord.Down: a concise, zero‑overhead solution for priority types and more.
12 Sept 2025, 07:35 UTC

Problem: Inverted Ordering in Priority Queues
When building a priority queue, a common pattern is to treat lower numeric values as higher priority. The built‑in Ord instance for Int does the opposite – it orders numbers ascending. Writing a custom instance for a wrapper type is trivial, but it duplicates boilerplate across many modules and can be error‑prone.
Thesis: DerivingVia Lets You Reuse Existing Instances
The DerivingVia extension, available since GHC 8.6, allows you to derive a typeclass instance by coercing your type to another that already implements the class. When the two types share the same runtime representation (e.g., two newtypes around Int), the compiler can perform the coercion automatically, and the derived instance behaves exactly like the original.
How DerivingVia Works Under the Hood
- Enable the extension:
{-# LANGUAGE DerivingVia #-}. - Declare a
newtypethat wraps the underlying type. - Use
deriving (Class) via OtherTypeto tell GHC to use the instance ofOtherType.
The compiler inserts a coerce call that converts between the two newtypes. Because newtype has no runtime overhead, the resulting instance is as efficient as hand‑written code.
Concrete Worked Example: Priority with Data.Ord.Down
Suppose we define a Priority type that should compare in reverse order to Int. The standard library provides Data.Ord.Down, a wrapper that flips comparisons. Using DerivingVia, we can derive Ord for Priority in one line.
{-# LANGUAGE DerivingVia #-}
import Data.Ord (Down(..))
newtype Priority = Priority Int
deriving (Show, Eq) -- normal instances
deriving Ord via Down Int -- flip ordering
To verify:
:set -XDerivingVia
:load PriorityModule
compare (Priority 1) (Priority 2)
-- Expected output: GT
Because Down Int implements Ord by reversing compare, the derived instance for Priority behaves accordingly. No manual compare or max functions are needed.
Extending the Pattern
- Monoid / Semigroup:
newtype SumInt = SumInt Int deriving (Semigroup, Monoid) via Sum Int. - Functor / Applicative: Not applicable because the via type must be a newtype over the same representation.
These patterns keep the code DRY and let the compiler handle the coercion details.
Trade‑offs and Limitations
- Readability: Future maintainers may not immediately see why
PriorityusesDown Intunless they are familiar withDerivingVia. Adding a comment or a small wrapper type can mitigate this. - Debugging: Compile errors may reference the coerced type, making it harder to trace back to the user type. Using
coercein anewtypekeeps the error messages close to the source. - Compatibility: The via type must be coercible to the target type. Arbitrary records or types with different layouts cannot use
DerivingViawithout a newtype wrapper. - Instance Availability:
DerivingViarelies on existing instances. If the desired instance does not exist for the via type, you must still write a manual instance.
Actionable Takeaways
- When you need a custom
Ordthat flips the default, considerDerivingViawithDown. - Use
newtypewrappers to keep the runtime representation identical; this guarantees zero overhead. - Document the via type clearly in the module header to aid future readers.
- Verify the derived instance by running a small test in GHCi or adding a quick unit test.
- If you find the error messages confusing, try writing a minimal hand‑coded instance first to understand the expected behavior.
DerivingVia is a powerful tool for reducing boilerplate while preserving efficiency. With careful documentation and a clear understanding of the coercion rules, it can simplify many common typeclass derivations in Haskell.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.