# Rank 'n Classy Limited Effects

Side effects are awful. Database access, HTTP requests, file reading, talking to Redis, ah! Just so much gross IO code to shuffle around. There’s been a lot of effort to make things nicer. Using monads to track effects in the type is a great start, but it’s a little painful to work with without some good abstractions.

The mtl library does a great job of making abstractions, but it has a big flaw: every new monad you want to introduce incurs $O(n^2)$ instances that you need to write. If you’re using Reader, State, Logger, Http, Database, Email, etc. (with special instances for testing/production/etc) then eventually this becomes too much of a burden.

More recently, the free monad approach and extensible-effects on top of it have become more popular. Free monads solve the $O(n^2)$ instance problem, and they offer the ability to introspect on the computation and perform optimizations on it. However, they have worse performance and are more complicated to implement. You either have to build a giant command functor with an equally complex interpreter, or you need to build many small languages and manage their combinations.

I’ve been working on a very promising pattern at the day job lately, and it’s worked out quite well thus far. It seems to solve the issues involved with a ridiculous proliferation of monad instances and the complications involved with free monads, while still giving most of the benefits of both.

# mtl style, revisited

If you’re unfamiliar, the mtl style of documenting effects is to use type classes to specify the effects of functions. This has two main benefits:

Concretely, these two functions are incompatible:

foo :: StateT Int (Reader Char) Bool
foo = do
int <- get
pure False

bar :: ReaderT Char (State Int) Bool
bar = do
int <- lift get
pure True


Since we have to specify the lifts, we can’t use them together. The mtl approach makes this possible:

foo :: (MonadState Int m, MonadReader Char m) => m Bool
foo = do
int <- get
pure False

bar = do
int <- get
pure True


The two functions are now easily interoperable! Nice. We also didn’t have to type lift a bunch, though truth be told, you just need the mtl type classes in scope for that to work. While this is nice, it’s not the real benefit of mtl.

## Strict specification of effects

When you use an mtl type class, you’re restricting yourself to the interface that the type class provides. If your monad is StateT Int IO String, then your monad can do any IO it wants. That’s no good! But if you know your function is MonadState Int m => m String, you know it can only operate on the state.

This lets you swap implementations easily. The PureScript compiler had an awesome demonstration, where they moved a WriterT based logger to an IO based instance (documented here) for big performance gains.

First, we’ll define a type class that represents an effect. We’ll use a limited subset of Http requests.

class Monad m => MonadHttp m where
get :: Url -> m ByteString
post :: ToJSON a => Url -> a -> m ByteString

type Url = String


We can easily make an instance for IO, using wreq:

instance MonadHttp IO where
get = fmap (view responseBody) . Wreq.get
post url = fmap (view responseBody) . Wreq.post url . toJson


Now, wherever we might have been using a function like makeRequest :: Something -> IO OtherThing, we can now abstract that IO into makeRequest :: MonadHttp m => SomeThing -> m OtherThing. We can make the change transparently, since IO will still be inferred and used. Plus, we have the assurance that we’re not going to be accessing the database or printing any output in our MonadHttp functions.

Actually running HTTP requests in dev/test is boring. It’s slow, annoying, unreliable, etc. and we’d much rather run locally for faster tests and more reliable development. We can easily create a mock implementation of MonadHttp that does static returns:

newtype MockHttp a
= MockHttp
{ runMockHttp :: ReaderT HttpEnv IO a

type HttpEnv = IORef HttpState
type HttpState = Map String ByteString

get url = do
pure (Map.lookup url state)
post url body = do
liftIO (writeIORef ref (Map.insert url (encode body) state))
pure "200 OK"


Now, with this instance, all of your MonadHttp requests will be performed real fast (and dumb).

# RankN Classy

Now, how can we select which interpretation we want? We obviously want IO for production and MockHttp for testing. Ultimately, what we want is one of a family of functions, with the following generalized type:

forall app a. (forall m. MonadHttp m => m a) -> app a


And here, we see RankNTypes come into play. The two type variables app and a are both Rank1 types, since they’re introduced at the leftmost part of the function and are mentioned to the right of all the function arrows. The m type variable, on the other hand, is hidden – that’s a Rank2 type variable. The variables app and a can both be chosen by the user to be whatever works, but we’re forcing the user to provide a value that not only satisfies the MonadHttp type class, but that it can do no more than MonadHttp. Consider this other signature:

forall app m a. MonadHttp m => m a -> app a


It looks really similar, but that m is no longer hidden. The user of the function can easily select IO as the implementation, as that satisfies the type class requirements. The user could then execute arbitrary IO actions, and the types haven’t helped much.

The Rank2 type above forces the user to only use MonadHttp functions and actions.

We can safely specialize app to IO for now, which we’ll need in order to read IORefs and do HTTP. So the functions we’re looking for, then are:

mockHttpRequests :: HttpEnv -> (forall m. MonadHttp m => m a) -> IO a
mockHttpRequests env action =

runHttpRequests :: (forall m. MonadHttp m => m a) -> IO a
runHttpRequests action = action


# Abstracting the implementations

Now, here’s the last bit of the trick: You abstract out the implementations into a record.

data Services
= Services
{ runHttp :: forall a. (forall m. MonadHttp m => m a) -> IO a
}


which you store in your application environment:

type Application = ReaderT Services IO


Now, when you need to run HTTP requests, you can do:

foobar :: Application Int
foobar = do
lift $runHttp service$ do
post "http://secret-data" (collectData page)
pure (length page)


Then, while initializing your application, you can choose which environment to pass in:

runApplicationProd :: Application a -> IO a
runApplicationProd action = runReaderT action (Services runHttpRequests)

runApplicationTest :: Application a -> IO a
runApplicationTest action = do
ref <- newIORef initialHttpState


Oh this is the best part!

reify :: (forall m. MonadHttp m => m a) -> Free HttpF a
reify = unFreeHttp

newtype FreeHttp a = FreeHttp { unFreeHttp :: Free HttpF a }

data HttpF next
= Get Url (ByteString -> next)
| forall a. FromJSON a => Post Url a next

get url = FreeHttp (liftF (Get url id))
post url body = FreeHttp (liftF (Post url body))


And now you can grab ahold of a Free monad representation of your AST.

# The Theory

Where a free monad seems like a great way to describe the effects of a computation, they seem to be more awkward at implementing requests. I’m inspired by Tomas Petricek’s Coeffects concept, which describes the context or environment of a computation as an indexed comonad. It seems like this approach allows you to request an environment comonad of interpreters for effects. By reifying these effects at the value level (a trick similar to Gabriel Gonzalez’s First Class Module Records), we avoid a lot of the problems with type classes and instances, while keeping the niceties of their abstractions.

An environment comonad is a essentially a really complicated way of saying “tuple”, and that’s left adjoint1 to a reader monad. We get nice syntax sugar for monads and not comonads in Haskell, so ReaderT Services provides a nice approach to packaging up your environment’s request context.

What’s next? Well, you might note that the Services type was a little restricted. Indeed, the following is a bit nicer:

data Services g
= Services
{ runHttp :: forall a. (forall f. MonadHttp f => f a) -> g a
}


Which, hey… That’s just a natural transformation! Specifically, a monad morphism. We can reify that type with ConstraintKinds to get:

type InterpreterFor g eff = forall a. (forall f. eff f => f a) -> g a


We can read this as: You choose the effect you want to interpret, and the monad you want to interpret it to. But you can’t choose the underlying concrete f, nor can you introspect on the as to do so.

If you allow TypeOperators, then it even reads nicely, and we can replace our IO services with:

data Services
= Services
{ runHttp :: IO InterpreterFor MonadHttp
}


(Not going to lie, that syntax really pleases my inner Rubyist)

And, in a final act of cutting IO out of the program, we can parametrize that, yielding:

data Services eff
= Services
{ runHttp     :: eff InterpreterFor MonadHttp
, runDatabase :: eff InterpreterFor MonadDatabase
, runEmails   :: eff InterpreterFor MonadMandrill
-- etc...
}


along with a final Application type that abstracts over that:

type Application = forall m. ReaderT (Services m) m


Applications, then, are just an environment comonad of monad morphisms. More plainly, they’re a record of effect interpreters.

# Update:

Thanks to /u/ElvishJerrico on Reddit who has implemented a Category instance for these morphisms! This is a great way to compose effects. The given example is copied here:

{-# LANGUAGE ConstraintKinds            #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE PolyKinds                  #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE UndecidableInstances       #-}

module Lib where

import           Control.Category
import           Prelude                hiding (id, (.))
import qualified Prelude                as P

newtype Interpret c d = Interpret (forall n a. d n => (forall m. c m => m a) -> n a)

instance Category Interpret where
id = Interpret P.id
Interpret f . Interpret g = Interpret $\h -> f (g h) class Monad m => MonadHttp m where httpGet :: String -> m String newtype HttpApp a = HttpApp { runHttpApp :: IO a } deriving (Functor, Applicative, Monad, MonadIO) instance MonadHttp HttpApp where httpGet _ = return "[]" -- Should do actual IO runIO :: Interpret MonadHttp MonadIO runIO = Interpret$ \x -> liftIO $runHttpApp x newtype MockHttp m a = MockHttp { runMockHttp :: m a } deriving (Functor, Applicative, Monad) instance MonadReader r m => MonadReader r (MockHttp m) where ask = MockHttp ask local f (MockHttp m) = MockHttp$ local f m

runMock = Interpret runMockHttp

getUserIds :: m [Int]

data RestApi a = GetUsers ([Int] -> a) deriving Functor

getUserIds = liftF $GetUsers id runRestApi :: Interpret MonadRestApi MonadHttp runRestApi = Interpret$ iterA go where