Support overriding "No instance" errors using the default deriving strategy
Motivation
Consider this code:
data FooState a b c d = FooState
{ partA :: a
, partB :: b
, partC :: c
, partD :: d
}
deriving (Show, Read)
withFoo :: FooState a b c d -> FooState a b c d
withFoo = undefined
Eugh, the API has 4 type params. This is confusing for users, especially when they combine it with other functions that take additional type params. In fact (a, b, c, d) are logically "our" (as the author of FooState
) type params, so we can simplify it like this, by grouping them together:
{-# LANGUAGE TypeFamilies #-}
class FooParams p where
type FooA p
type FooB p
type FooC p
type FooD p
data FooState' p = FooState'
{ partA :: FooA p
, partB :: FooB p
, partC :: FooC p
, partD :: FooD p
}
-- deriving (Show, Read) -- fails!
withFoo' :: FooParams p => FooState' p -> FooState' p
withFoo' = undefined
(Alternatively, using more advanced language features you can even avoid the typeclass context, although this prevents you defining class methods on FooA etc, click for details)
{-# LANGUAGE TypeFamilies, RankNTypes, PolyKinds, DataKinds, StandaloneKindSignatures #-}
data FooParams aT bT cT dT = FooParams
{ fooA :: aT
, fooB :: bT
, fooC :: cT
, fooD :: dT
}
type FooA :: forall a b c d. FooParams a b c d -> *
type family FooA p where FooA ('FooParams a b c d) = a
type FooB :: forall a b c d. FooParams a b c d -> *
type family FooB p where FooB ('FooParams a b c d) = b
type FooC :: forall a b c d. FooParams a b c d -> *
type family FooC p where FooC ('FooParams a b c d) = c
type FooD :: forall a b c d. FooParams a b c d -> *
type family FooD p where FooD ('FooParams a b c d) = d
data FooState' (p :: FooParams a b c d) = FooState'
{ partA :: FooA p
, partB :: FooB p
, partC :: FooC p
, partD :: FooD p
}
-- deriving (Show, Read) -- fails!
withFoo' :: FooState' p -> FooState' p
withFoo' = undefined
Either way, great, our API now only has 1 type param.
Unfortunately, now deriving (Show, Read)
fails (in both examples):
Test.hs:26:13: error:
• No instance for (Show (FooA p))
arising from the first field of ‘FooState'’ (type ‘FooA p’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Show (FooState' p))
|
26 | deriving (Show, Read)
| ^^^^
Test.hs:26:19: error:
• No instance for (Read (FooA p))
arising from the first field of ‘FooState'’ (type ‘FooA p’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Read (FooState' p))
|
26 | deriving (Show, Read)
| ^^^^
We can currently work around this in 2 ways, neither of which is ideal:
standalone deriving, tedious manual context
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE StandaloneDeriving, FlexibleContexts, UndecidableInstances #-}
class FooParams p where
type FooA p
type FooB p
type FooC p
type FooD p
data FooState' p = FooState'
{ partA :: FooA p
, partB :: FooB p
, partC :: FooC p
, partD :: FooD p
}
-- manually supply the context, eugh
-- increases n*m with number of associated types, and the number of extra instances e.g. Eq, Ord
deriving instance (Show (FooA p), Show (FooB p), Show (FooC p), Show (FooD p)) => Show (FooState' p)
deriving instance (Read (FooA p), Read (FooB p), Read (FooC p), Read (FooD p)) => Read (FooState' p)
withFoo' :: FooParams p => FooState' p -> FooState' p
withFoo' = undefined
extra type synonym, slightly ugly API
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
class FooParams p where
type FooA p
type FooB p
type FooC p
type FooD p
-- data decl still has 4 type params
data FooState a b c d = FooState
{ partA :: a
, partB :: b
, partC :: c
, partD :: d
}
deriving (Show, Read)
-- apply the type family in a synonym, and expose this along with
-- the constructor of the original data type if necessary.
-- however, this gets ugly if the data structure is more complex
type FooState'' p = FooState (FooA p) (FooB p) (FooC p) (FooD p)
-- API methods can use the type synonym to reduce the number of params
withFoo'' :: FooParams p => FooState'' p -> FooState'' p
withFoo'' = undefined
Proposal
I guess the error is a purposeful design choice to avoid surprising users - the same error occurs if e.g. using IO p
instead of FooA p
, and the same workarounds also work. Clearly, successfully and silently generating an instance with the context (Show (IO p) .. ) =>
, would be surprising to users.
However for certain use-cases such as the one I just described, this "potentially-surprising" derived instance is in fact what we want, and is not surprising for these particular cases (e.g. think Maybe p
). So it would be good if we could override the error and tell GHC to just use what it would have output. This could possibly be implemented as a new strategy with a name such as "stock_relaxed" or "stock_unchecked" or something.