This short post is part of the Practical Haskell Bits initiative. Visit the repository to find out more real-world examples like this.
Imagine we have defined a typeclass for communicating with some external API.
class ExternalAPI m where
getExternal :: ExternalThingId -> m ExternalThingWe define some abstract implementation:
getExternalImpl ::
MonadIO m =>
ExternalThingId ->
m ExternalThing
getExternalImpl = ...And add an instance to our application monad:
instance ExternalAPI AppM where
getExternal = getExternalImplBut what if we need an instance of ExternalAPI for some other monad as well?
instance ExternalAPI AnotherAppM where
getExternal = getExternalImplThis is not too terrible, but now every time we change the ExternalAPI interface, we need to change the instance definitions as well. Also, we need to export and depend on implementation details such as getExternalImpl, which is error prone and can quickly get tedious.
Using DerivingVia, we can make this a lot more elegant.
Let’s define 2 abstract implementations for ExternalAPI: one mocked for testing, and one real one for production
-- Mocked implementation
newtype MockedExternalAPI m a = MockedExternalAPI (m a)
instance (MonadIO m) => ExternalAPI (MockedExternalAPI m) where
getExternal externalId = MockedExternalAPI $ do
liftIO $ putStrLn $ "Called mocked getExternal with id " <> show externalId <> "..."
pure ExternalThing
postExternal _ = MockedExternalAPI $ do
liftIO $ putStrLn "Called mocked postExternal..."
pure 1
-- "Real" implementation
newtype RealExternalAPIClient m a = RealExternalAPIClient (m a)
instance (MonadIO m) => ExternalAPI (RealExternalAPIClient m) where
getExternal externalId = RealExternalAPIClient $
liftIO $ do
putStrLn $ "Calling real API with id " <> show externalId <> "..."
threadDelay $ 1000 * 500 -- Half a second
pure ExternalThing
postExternal _ = RealExternalAPIClient $
liftIO $ do
putStrLn "Posting to the real API..."
threadDelay $ 1000 * 500
pure 123Things have become a tad more abstract.
We have defined two wrappers: MockedExternalAPI and RealExternalAPIClient and said that if the inner m has a MonadIO instance, we can give back an ExternalAPI implementation.
Now for our application monads we can do
-- The real application monad that handles your business logic
newtype AppM a = AppM {runAppM :: IO a}
...
deriving (ExternalAPI) via (RealExternalAPIClient AppM)
-- A monad that you use for testing
newtype TestAppM a = TestAppM {runTestAppM :: IO a}
...
deriving (ExternalAPI) via (MockedExternalAPI TestAppM)This clears up a few things:
- We don’t need to depend on implementation details such as
getExternalImpl. - We have reduced boilerplate.
- We don’t need to change every instance site when we change the
ExternalAPIinterface.
demo :: IO ()
demo = do
runAppM externalAPIAction
runTestAppM externalAPIAction
where
externalAPIAction ::
Monad m =>
ExternalAPI m =>
m ()
externalAPIAction = do
void $ getExternal 123
void $ postExternal ExternalThingYou can find the complete example here.
This short post is part of the Practical Haskell Bits initiative. Visit the repository to find out more real-world examples like this.