Skip to content

Instantly share code, notes, and snippets.

@gelisam
Created May 21, 2020 14:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gelisam/caf8fbd77ce1d43f66c215edea1e3268 to your computer and use it in GitHub Desktop.
Save gelisam/caf8fbd77ce1d43f66c215edea1e3268 to your computer and use it in GitHub Desktop.
Using a runtime value to choose a monad transformer stack
-- in response to https://twitter.com/1akrmn/status/1263124698449272832
--
-- We have a polymorphic action which can be instantiated to multiple
-- transformer stacks, and the goal is to use a runtime value to
-- determine which monad transformer stack to use.
{-# LANGUAGE AllowAmbiguousTypes, FlexibleContexts, FlexibleInstances, GeneralizedNewtypeDeriving, MultiParamTypeClasses, QuantifiedConstraints, RankNTypes, StandaloneDeriving, TypeApplications, UndecidableInstances #-}
{-# OPTIONS -Wno-orphans #-}
module Main where
import Test.DocTest
import Prelude hiding (log)
import Control.Monad.IO.Class
import Control.Monad.Writer hiding (Product)
import Data.Functor.Product
-- $setup
-- >>> :set -XFlexibleContexts
-- First, let's define the problem. We have a typeclass for a monad
-- transformer...
class MonadLog m where
log :: String -> m ()
-- Which can be either be instantiated with ConsoleLogT...
newtype ConsoleLogT m a = ConsoleLogT { runConsoleLogT :: m a }
deriving (Functor, Applicative, Monad)
instance MonadIO m => MonadLog (ConsoleLogT m) where
log s = ConsoleLogT $ liftIO $ putStrLn s
deriving instance MonadWriter w m => MonadWriter w (ConsoleLogT m)
-- Or with NoLogT...
newtype NoLogT m a = NoLogT { runNoLogT :: m a }
deriving (Functor, Applicative, Monad)
instance Monad m => MonadLog (NoLogT m) where
log _ = pure ()
deriving instance MonadWriter w m => MonadWriter w (NoLogT m)
-- |
-- We also have a polymorphic action which can either be instantiated
-- with ConsoleLogT or with NoLogT:
--
-- >>> execWriterT $ runConsoleLogT action
-- begin
-- end
-- "middle"
-- >>> execWriterT $ runNoLogT action
-- "middle"
action :: (MonadLog m, MonadWriter String m) => m ()
action = do
log "begin"
tell "middle"
log "end"
-- |
-- But we would like to use some runtime configuration variable, a
-- Bool, to decide which monad transformer stack to use. The following
-- attempt seems promising:
--
-- >>> runAction True $ log "hello"
-- hello
-- >>> runAction False $ log "hello"
runAction :: Bool
-> (forall m. MonadLog m => m a)
-> IO a
runAction True body = runConsoleLogT body
runAction False body = runNoLogT body
-- |
-- But unfortunately, the type @forall m. MonadLog m => m a@ means
-- that 'body' must work for _any_ 'm' which has a 'MonadLog'
-- instance, including @NoLogT Identity@. This is unfortunate, because
-- our 'action' only works with monad stacks which contain a 'WriterT'
-- layer.
--
-- >>> runAction True action
-- ...
-- ... Could not deduce (MonadWriter String ...
-- ...
-- My solution is to use Data.Functor.Product to instantiate the
-- body's 'm' with two monad transformer stacks at the same time.
--
-- data Product m n a = Pair (m a) (n a)
-- I must first write some instances to show that
-- @Product (ConsoleLogT m) (NoLogT m)@ is an appropriate monad
-- transformer stack for 'action':
instance (MonadLog m, MonadLog n)
=> MonadLog (Product m n) where
log s = Pair (log s) (log s)
instance (MonadWriter w m, MonadWriter w n)
=> MonadWriter w (Product m n) where
tell w = Pair (tell w) (tell w)
listen (Pair ma na) = Pair (listen ma) (listen na)
pass (Pair ma na) = Pair (pass ma) (pass na)
-- |
-- And now I can easily write a function which instantiates 'action'
-- with those two monad transformer stacks, and uses a Bool to
-- determine which instantiation to keep:
--
-- >>> execWriterT $ runLogT True action
-- begin
-- end
-- "middle"
-- >>> execWriterT $ runLogT False action
-- "middle"
runLogT :: MonadIO m
=> Bool -> Product (ConsoleLogT m) (NoLogT m) a -> m a
runLogT True (Pair body _) = runConsoleLogT body
runLogT False (Pair _ body) = runNoLogT body
main :: IO ()
main = do
doctest ["src/Main.hs"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment