Reuse Your Monadic Code

Roman Cheplyaka
@shebang

Intro

Coming from Odessa, Ukraine

Intro

Doing Haskell for 6+ years

Intro

Now work for

Student counseling through automated text messages

Testing campaigns

Messaging campaigns are written in an in-house DSL

How do you test them?

You create a simulator!

Challenge

Have the server and simulator use the same code

Challenge

Effect Simulator Server
Logging Yes Yes
Current time Yes Yes
SMS send/recv Yes Yes
Console I/O Yes No
MQ No Yes

Class-based approach

handleMessage 
  :: ( MonadWriter LogEntry m
     , MonadWriter MessageText m
     , MonadState Image m
     , MonadReader Messages m
     , MonadReader Now m
     , MonadReader SQL.Connection m
     , MonadExcept DynamicTypeError m
     , MonadExcept EvalError m
     , MonadExcept ProgramError m
     ) => m ()
handleMessage = ...

MonadReader needs your help

data Nat = Zero | Suc Nat

class MonadReaderN (n :: Nat) r m where
  askN :: Proxy n -> m r

instance MonadReaderN Zero e (ReaderT e m) where
  askN _ = ask

instance (MonadTrans t,
          MonadReaderN n r m)
       => MonadReaderN (Suc n) r (t m)
  where
    askN _ = lift $ askN (Proxy :: Proxy n)

Closed type families

type family
  Find
    (t :: (* -> *) -> (* -> *))
    (m :: * -> *)
    :: Nat where
  Find t (t m) = Zero
  Find t (p m) = Suc (Find t m)

type MonadReader r m =
  MonadReaderN (Find (ReaderT r) m) r m

ask :: forall r m. MonadReader r m => m r
ask = askN (Proxy :: Proxy (Find (ReaderT r) m))

Effectful transformers

type family
  CanDo
    (t :: (* -> *))
    (eff :: k)
    :: Bool

data EffState s
data EffReader e
data EffWriter w

type family Find eff (m :: * -> *) :: Nat

State vs Reader

foo :: (MonadState User m, ...) => m ()

bar :: (MonadReader User m, ...) => m ()

baz = do
  foo
  bar

State as Reader

type instance CanDo (StateT s m) eff = StateCanDo s eff

type family StateCanDo s eff where
  StateCanDo s (EffState s) = True
  StateCanDo s (EffReader s) = True
  StateCanDo s eff = False

instance MonadReaderN Zero r (StateT r m) where
    askN _ = get

Zooming

newtype ZoomT big small m a = ZoomT (...)
  deriving (Functor, Applicative, Monad, ...)

instance
  MonadReader big m =>
  MonadReaderN Zero small (ZoomT big small m)

runZoom
  :: Lens big small
  -> ZoomT big small m a
  -> m a
runZoom l a = ...

Drawbacks

x = runState get 0
No instance for (MonadStateN
                   (FindTrue
                      '[monad-classes-0.1:Control.Monad.Classes.State.StateCanDo
                          s0 (EffState a0),
                        CanDo Data.Functor.Identity.Identity (EffState a0)])
                   a0
                   (Control.Monad.Trans.State.Lazy.StateT
                      s0 Data.Functor.Identity.Identity))
  arising from a use of ‘it’
The type variables ‘s0’, ‘a0’ are ambiguous
Note: there are several potential instances:
  instance (Monad (t m), Control.Monad.Trans.Class.MonadTrans t,
            MonadStateN n s m, Monad m) =>
           MonadStateN ('Suc n) s (t m)
    -- Defined in ‘monad-classes-0.1:Control.Monad.Classes.State’
  instance Monad m =>
           MonadStateN 'Zero s (Control.Monad.Trans.State.Lazy.StateT s m)
    -- Defined in ‘monad-classes-0.1:Control.Monad.Classes.State’
In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it