Deriving strategy
A deriving clause asks the compiler to write an instance. Haskell 98 allowed this for a fixed set of classes, such as Eq, Ord and Show, and generated code from the constructors of the type[4]. GHC added further mechanisms: generalized newtype deriving reuses the instance of the type a newtype wraps, DeriveAnyClass writes an empty instance that relies on default methods, often generic ones[5], and DerivingVia reuses the instance of any type with the same representation[1]. For a newtype, more than one of these usually applies and they give different instances, so GHC 8.2 added the DerivingStrategies syntax to name the one intended[2]. The coercing strategies are free at run time, and their safety rests on type roles[3].
Example
The four strategies on two newtypes.
{-# LANGUAGE DerivingStrategies, GeneralizedNewtypeDeriving, DeriveAnyClass, DerivingVia, DefaultSignatures #-}import Data.Monoid (Sum (..), Product (..))-- A class with a default method, for anyclass deriving.class Describe a wheredescribe :: a -> Stringdefault describe :: Show a => a -> Stringdescribe x = "<" ++ show x ++ ">"newtype Age = Age Intderiving stock (Show, Eq) -- structural: shows as Age 3deriving newtype (Num, Ord) -- Int's instances, by coercionderiving anyclass Describe -- the class's default methodsderiving (Semigroup, Monoid) via (Sum Int) -- Sum Int's instancesnewtype Score = Score Intderiving newtype Show -- Int's Show: shows as 3deriving (Semigroup, Monoid) via (Product Int)main :: IO ()main = doprint (Age 3, Score 3)print (Age 3 + 4, Age 3 < Age 4)putStrLn (describe (Age 3))print (mconcat [Age 1, Age 2, Age 3], mempty :: Age)print (mconcat [Score 2, Score 3, Score 4], mempty :: Score)
Running it.
(Age 3,3)(Age 7,True)<Age 3>(Age 6,Age 0)(24,1)
The distinction is not cosmetic. stock Show prints Age 3 and newtype Show prints 3; via (Sum Int) makes <> addition while via (Product Int) makes it multiplication, from types with the same representation.
The four strategies
Stock
The compiler generates code by structural recursion over the constructors, as the Haskell report specifies for Eq, Ord, Enum, Bounded, Show, Read and Ix[4]. GHC extends stock deriving to Functor, Foldable, Traversable, Generic, Data and Lift.
A small type.
module Stock wheredata Shape = Circle Double | Rect Double Double deriving Eq
The Eq instance GHC generates, as printed by -ddump-deriv.
Derived class instances:instance Eq Shape where(==) a b= case(case (dataToTag# a) ofa# -> case (dataToTag# b) of b# -> a# /=# b#)of1# -> False_ -> case a of(Circle a1) -> case b of (Circle b1) -> ((a1 == b1))(Rect a1 a2)-> case b of (Rect b1 b2) -> (((a1 == b1)) && ((a2 == b2)))
The generated code first compares constructor tags, then the fields of equal constructors, which is what a careful hand-written instance would do.
Newtype
For newtype Age = Age Int, generalized newtype deriving reuses Int's dictionary. A newtype has the same representation as the type it wraps, so the Num Int dictionary can be coerced to a Num Age dictionary without generating any code, and the derived methods run exactly as fast as Int's.
Anyclass
The instance is empty, so every method takes the class's default. This is useful for classes whose defaults are defined generically, through Generic and DefaultSignatures, as in serialization libraries: deriving anyclass (ToJSON) gives a JSON encoder computed from the type's generic representation[5].
Via
deriving C via V reuses V's instance for any V with the same representation as the type being defined[1]. The Sum and Product wrappers exist to select monoids in this way, and libraries define newtypes whose only purpose is to carry an instance to be borrowed: a type can derive Semigroup via Max Int, Show via a wrapper that prints in hexadecimal, or Num for a tuple via an applicative lifting. Blöndal, Löh and Scott argued that this turns many hand-written instances into a single line[1].
Ambiguity
Without an explicit strategy, GHC uses stock deriving for the standard classes and otherwise chooses by the extensions in effect. When both GeneralizedNewtypeDeriving and DeriveAnyClass are on, a newtype deriving a class with defaults could use either[2].
Two applicable strategies with different results.
{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveAnyClass #-}class Describe a wheredescribe :: a -> Stringdescribe _ = "something"instance Describe Int wheredescribe n = "the number " ++ show n-- Both newtype deriving and anyclass deriving apply to Describe.newtype Age = Age Int deriving Describemain :: IO ()main = putStrLn (describe (Age 3))
GHC's warning.
ambiguous.hs:10:32: warning: [-Wderiving-defaults]* Both DeriveAnyClass and GeneralizedNewtypeDeriving are enabledDefaulting to the DeriveAnyClass strategy for instantiating Describe* In the newtype declaration for `Age'Suggested fix:
Running it: the class default, not Int's instance.
something
Writing deriving newtype Describe or deriving anyclass Describe makes the choice explicit and silences the warning.
Roles and soundness
The coercing strategies are sound only if every method of the class can be coerced from the wrapped type to the newtype. That fails when a method mentions the type under a type constructor whose invariants depend on the exact type. Set Int and Set Age have the same representation, but a set's ordering comes from its element type's Ord instance, so coercing one to the other could break the tree's invariant. GHC tracks this with roles: Set's parameter has the nominal role, and a coercion that changes it is rejected[3].
A class whose method mentions Set a.
{-# LANGUAGE DerivingStrategies, GeneralizedNewtypeDeriving #-}import qualified Data.Set as Setclass Contains a wherecontains :: Set.Set a -> a -> Boolinstance Contains Int wherecontains s x = Set.member x s-- Coercing Set Int -> Bool to Set Age -> Bool is not allowed: Set's-- parameter has the nominal role, because its invariant depends on Ord.newtype Age = Age Int deriving newtype Contains
GHC's error.
roles.hs:12:40: error:* Couldn't match type `Int' with `Age'arising from the coercion of the method `contains'from type `Set.Set Int -> Int -> Bool'to type `Set.Set Age -> Age -> Bool'* When deriving the instance for (Contains Age)|12 | newtype Age = Age Int deriving newtype Contains| ^^^^^^^^
Before roles, generalized newtype deriving could be combined with type families to coerce between unrelated types, breaking type safety. Roles, introduced in GHC 7.8 in 2014, closed that hole by distinguishing nominal, representational and phantom parameters[3][6].
In other languages
Rust's #[derive] attributes correspond to stock deriving, and procedural macros let libraries add derivable traits, much like anyclass deriving through generics. OCaml has no built-in deriving; ppx preprocessors such as ppx_deriving generate the code from type declarations at compile time. Neither language has an equivalent of newtype or via deriving, since neither has zero-cost coercions between a type and its wrapper that the type checker can reason about.
History
Haskell 1.0 in 1990 included deriving for a fixed set of classes, and the Haskell 98 report specifies the generated code[4]. GHC added generalized newtype deriving in the early 2000s; its interaction with type families was found to be unsound, which led to the work on generative type abstraction and to roles in GHC 7.8[6][3]. DeriveAnyClass arrived with GHC's generic programming support[5]. DerivingStrategies was added in GHC 8.2 in 2017 and DerivingVia in GHC 8.6 in 2018[1][2].
see also
- ppxA preprocessor that rewrites the OCaml syntax tree between parsing and type checking. Derivers generate code from type declarations tagged [@@deriving name]; extension points expand nodes written [%name payload]. Neither sees any types.
- Orphan instanceAn orphan instance is a type class instance declared in a module that defines neither the class nor the type. Because instance resolution is global but imports are not, two orphans for the same class and type can coexist in one program, and code compiled against different ones can disagree: a Set built under one ordering and searched under another silently loses elements. GHC warns about orphans, and Rust forbids them outright with its orphan rule. The usual fix is a newtype, which gives the instance a type of its own.
- Phantom typeA phantom type is a type parameter that appears in a type's declaration but in none of its fields, used to keep otherwise identical representations apart: lengths in metres and in feet, validated and unvalidated strings, read-only and writable handles, SQL expressions of different result types. It carries no data and costs nothing at run time. Combined with an abstract type whose constructors are hidden, it lets a library enforce invariants through the type checker. Unlike a GADT index it tells a pattern match nothing; it constrains what callers may do, not what the compiler can learn.
- Dictionary passingDictionary passing is the standard way to implement type classes. Each class becomes a record type whose fields are its methods, each instance becomes a value of that record type, and each function with a class constraint receives the record, the dictionary, as an extra argument, which the compiler supplies at every call site after resolving which instance applies. An instance that itself has a context, such as equality on lists given equality on elements, becomes a function from dictionaries to a dictionary, and a superclass becomes a field. The same technique written by hand is the usual way to get type-class-like overloading in OCaml, with records, first-class modules or functors.
- Overlapping instancesTwo type class instances overlap when some type matches both of their heads, as Pretty [a] and Pretty String do at [Char]. Instance resolution in Haskell does not backtrack, so by default GHC reports an error rather than choose. The OVERLAPPING and OVERLAPPABLE pragmas let the most specific matching instance win, which is predictable when the choice is made at a known type and all the instances are in scope, but can make the same expression behave differently in different modules. INCOHERENT lets GHC choose even when the choice depends on a type it does not yet know. Closed type families and newtypes are the usual alternatives.
referenced by
further reading
- [1]B. Blöndal, A. Löh, R. Scott, “Deriving Via: or, how to turn hand-written instances into an anti-pattern”, Haskell Symposium (2018).
- [2]GHC User’s Guide, “Deriving strategies”.
- [3]J. Breitner, R. A. Eisenberg, S. Peyton Jones, S. Weirich, “Safe zero-cost coercions for Haskell”, Journal of Functional Programming 26 (2016).
- [4]S. Peyton Jones (ed.), Haskell 98 Language and Libraries: The Revised Report, ch. 10, Cambridge University Press (2003).
- [5]J. P. Magalhães, A. Dijkstra, J. Jeuring, A. Löh, “A generic deriving mechanism for Haskell”, Haskell Symposium (2010).
- [6]S. Weirich, D. Vytiniotis, S. Peyton Jones, S. Zdancewic, “Generative type abstraction and type-level computation”, POPL (2011).
last updated