15 Jan 2023

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!
Tags: haskell monad_transformers
Comment here.