Composing instances using deriving via
Today I watched the very good, and short, video from Tweag on how to Avoid boilerplate instances with -XDerivingVia. It made me realise that I've read about this before, but then the topic was on reducing boilerplate with MTL-style code.
Given that I'd forgotten about it I'm writing this mostly as a note to myself.
The example from the Tweag video, slightly changed
The code for making film ratings into a Monoid
, when translated to the UK,
would look something like this:
{-# LANGUAGE DerivingVia #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module DeriveMonoid where newtype Supremum a = MkSup a deriving stock (Bounded, Eq, Ord) deriving newtype (Show) instance Ord a => Semigroup (Supremum a) where (<>) = max instance (Bounded a, Ord a) => Monoid (Supremum a) where mempty = minBound data FilmClassification = Universal | ParentalGuidance | Suitable12 | Suitable15 | Adults | Restricted18 deriving stock (Bounded, Eq, Ord) deriving (Monoid, Semigroup) via (Supremum FilmClassification)
Composing by deriving
First let's write up a silly class for writing to stdout
, a single operation will do.
class Monad m => StdoutWriter m where writeStdoutLn :: String -> m ()
Then we'll need a type to attach the implementation to.
newtype SimpleStdoutWriter m a = SimpleStdoutWriter (m a) deriving (Functor, Applicative, Monad, MonadIO)
and of course an implementation
instance MonadIO m => StdoutWriter (SimpleStdoutWriter m) where writeStdoutLn = liftIO . putStrLn
Now let's create an app environment based on ReaderT
and use deriving via
to
give it an implementation of StdoutWriter
via SimpleStdoutWriter
.
newtype AppEnv a = AppEnv {unAppEnv :: ReaderT Int IO a} deriving ( Functor , Applicative , Monad , MonadIO , MonadReader Int ) deriving (StdoutWriter) via (SimpleStdoutWriter AppEnv)
Then a quick test to show that it actually works.
λ> runReaderT (unAppEnv $ writeStdoutLn "hello, world!") 0 hello, world!