Haskell’s type classes and deriving facilities are a killer feature for type safety and extensibility.
Over nearly 30 years they’ve acquired quite a bit of cruft and language extensions.
With DerivingVia
, we now have the ability to dramatically simplify the deriving story.
This post outlines a change to the language that would hopefully be adopted with the next version of the language standard. They get less reasonable and more dramatic as the post goes on.
GHC has a ton of extensions that only serve to unlock additional type classes to the “stock” deriving strategy.
Derive{Functor,Foldable,Traversable,Generic,Lift,etc}
.
We can remove all of these extensions by folding them into the stock
deriving strategy.
DeriveAnyClass
DeriveAnyClass
is a footgun.
It allows you to write any type class in a deriving
clause.
It pastes in an “empty” instance, relying on DefaultSignatures
to fill in the values.
-- With DeriveAnyClass:
data X = X
deriving ToJSON
-- Without:
data X = X
instance ToJSON X
DefaultSignatures
DefaultSignatures
is used to give a single default implementation of a type class if the underlying type matches a more restrictive constraint.
This is primarily used to provide Generic
-based implementations with very little syntax.
data X = X deriving Generic
-- with DefaultSignatures:
class ToJSON a where
toJSON :: a -> Value
default toJSON :: (Generic a, GToJSON (Rep a)) => a -> Value
toJSON = gtoJSON
instance ToJSON X
-- without DefaultSignatures:
class ToJSON a where
toJSON :: a -> Value
instance ToJSON X where
toJSON = gtoJSON
By privileging a single default, it makes any other possible defaults less useful and less discoverable.
The DeriveAnyClass
utility is subsumed by DerivingVia
.
newtype Generically a = Generically a
instance (Generic a, GToJSON (Rep a)) => ToJSON (Generically a) where
toJSON (Generically a) = gtoJSON a
data X = X
deriving stock Generic
deriving ToJSON via Generically X
GeneralizedNewtypeDeriving
This extension is subsumed by DerivingVia
, also.
-- with GeneralizedNewtypeDeriving
newtype UserId = UserId Text
deriving newtype (Show, ToJSON)
-- with DerivingVia
newtype UserId = UserId Text
deriving (Show, ToJSON) via Text
DerivingStrategies
Now that there’s only two strategies, we can get rid of DerivingStrategies
.
-- Before
data X = X
deriving stock (Show, Generic)
deriving (ToJSON, FromJSON) via Generically X
-- After
data X = X
deriving (Show, Generic)
deriving (ToJSON, FromJSON) via Generically X
Currently, you must write the complete type in a DerivingVia
clause.
data X = X
deriving Generic
deriving ToJSON via Generically X
newtype Y = Y Text
deriving ToJSON via Text
This can be cumbersome for a very large type.
newtype App a = App (ExceptT () (StateT () (ReaderT () IO)) a)
deriving
( Functor
, Applicative
, Monad
, MonadReader ()
, MonadError ()
, MonadState ()
)
via
ExceptT () (StateT () (ReaderT () IO))
It’s also annoyingly repetitive, and can lead to errors.
data Foo = Foo
deriving Generic
deriving ToJSON via Generically Foo
-- copy/paste error
data Bar = Bar
deriving Generic
deriving ToJSON via Generically Foo
A wildcard can be used to indicate either:
a. The underlying type of a newtype
, or
b. The type of the data
declaration.
data Foo = Foo
deriving Generic
deriving ToJSON via Generically _
-- no more copy paste error
data Bar = Bar
deriving Generic
deriving ToJSON via Generically _
-- mmmm nice and clean
newtype App a = App (ExceptT () (StateT () (ReaderT () IO)) a)
deriving
( Functor
, Applicative
, Monad
, MonadReader ()
, MonadError ()
, MonadState ()
)
via _
There are two ways to derive things: StandaloneDeriving
and attached deriving.
Attached deriving is redundant, but convenient.
StandaloneDeriving
is more powerful, but less convenient.
Attached deriving clauses don’t work with GADTs
.
-- Before:
data Foo = Foo
deriving Generic
deriving (FromJSON, ToJSON) via Generically _
newtype App a = App (ExceptT () (StateT () (ReaderT () IO)) a)
deriving
( Functor
, Applicative
, Monad
, MonadReader ()
, MonadError ()
, MonadState ()
)
via _
-- Only standalone:
data Foo = Foo
deriving instance Generic Foo
deriving via Generically _ instance ToJSON Foo
deriving via Generically _ instance FromJSON Foo
newtype App a = App ...
deriving via _ instance Functor App
deriving via _ instance Applicative App
deriving via _ instance Monad App
deriving via _ instance MonadReader () App
deriving via _ instance MonadError () App
deriving via _ instance MonadState () App
-- GADT must use standalone to specify a context
data Some f where
Some :: Show a => f a -> Some f
deriving instance (forall a. Show a => Show (f a)) => Show (Some f)
The problem with the above proposal is that it carries a significant syntax cost.
The keyword deriving
is repeated for each instance, the keyword instance
is repeated, the via _
clause is repeated, and the type name is repeated.
Multiple instances should be derivable with the same context.
data Foo = Foo
deriving Foo
( Generic
, via
(Generically _)
( ToJSON
, FromJSON
)
)
In this block, we define the ToJSON
and FromJSON
instances using the same Generically
viatype.
We can still use _
to refer to the type, since we know the type we’re deriving for: Foo
.
This recovers the syntax convenience of “attached deriving.”
newtype App a = App ...
deriving App
via _
instance
( Functor
, Applicative
, Monad
, MonadReader ()
, MonadError ()
, MonadState ()
)
This also recovers the convenience of attached deriving.
Let’s look at the main point - GADTs.
Otherwise we could just remove StandaloneDeriving
(with the nice benefit/tragedy of banning orphan derived instance).
data Some f where
Some :: Show a => f a -> Some f
-- old
deriving
instance (forall a. Show a => Show (f a)) => Show (Some f)
-- new
deriving Some
(forall a. Show a => Show (f a)) => Show (_ f)
-- generally,
deriving SomeGadtType
(SomeContextOn a b c) =>
( Show, Eq, ToJSON, FromJSON
)
(_ a b c)
The _
refers to the type name, without any variables applied.
So you need to apply the type variables in the instance head.
That’s a bit annoying, but maybe it’s fine
GHC provides a newtype Stock a = Stock a
that hooks in to DerivingVia
somehow.
Now we’re down to one deriving strategy.
data X = X
deriving X
via Stock _
( Eq, Show, Generic )
via Generically _
( ToJSON, FromJSON )
This “deprivileges” the Stock
deriving classes.
OK, so maybe you don’t like getting rid of attached deriving.
Let’s get rid of standalone deriving instead.
We need StandaloneDeriving
for two reasons:
data Some f where
Some :: Show a => f a -> Some f
deriving
( (forall a. Show a => Show (f a))
=>
Show
, -- generally,
(SomeContext f)
=>
SomeClass
)
The type variable f
is in scope from the data
declaration.
EDIT: @quickdudley and @nnotm have correctly pointed out that you also want to be able to define instances of a class at the definition module of a class.
These are perfectly valid instances, and so we must keep StandaloneDeriving
.
Alright, post is done. These ideas are certainly controversial and Bad, but man wouldn’t it be nice to have a simpler story around deriving and type class instances? The current story is so complex, and I think we can genuinely simplify Haskell-the-language by trimming some fat here.
EDIT: @i_am_tom posted a reference to the Concrete Class Dictionaries GHC proposal, which subsumes a lot of this.