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!