3
votes

I have been watching Refactoring some Haskell code to use MTL which refactors some Haskell code to make use of the typeclasses from the mtl package.

The code includes a postReservation function with the following signature:

postReservation :: ReservationRendition -> IO (HttpResult ())

The implementation of the postReservation function makes use of three additional functions with the following signatures:

readReservationsFromDB :: ConnectionString -> ZonedTime -> IO [Reservation]
getReservedSeatsFromDB :: ConnectionString -> ZonedTime -> IO Int
saveReservation :: ConnectionString -> Reservation -> IO ()

In the video, the signatures of the three functions are refactored so that they return a generic type with a MonadIO constraint i.e.

readReservationsFromDB :: (MonadIO m) => ConnectionString -> ZonedTime -> m [Reservation]
getReservedSeatsFromDB :: (MonadIO m) => ConnectionString -> ZonedTime -> m Int
saveReservation :: (MonadIO m) => ConnectionString -> Reservation -> m ()

I understand that doing this makes the functions more flexible as they no longer depend on a concrete monad type or a specific monad transformer stack configuration. I also understand that the postReservation function can still make use of these functions without any changes to its type signature because it has a return type of IO which is an instance of the MonadIO typeclass.

Next, the three functions are refactored to include a MonadReader constraint so that the connection string does not need to be explicitly passed around i.e.

readReservationsFromDB :: (MonadReader ConnectionString m, MonadIO m) => ZonedTime -> m [Reservation]
getReservedSeatsFromDB :: (MonadReader ConnectionString m, MonadIO m) => ZonedTime -> m Int
saveReservation :: (MonadReader ConnectionString m, MonadIO m) => Reservation -> m ()

The signature of the postReservation function is also updated to include the MonadIO and MonadReader constraints i.e

postReservation :: (MonadReader ConnectionString m, MonadIO m) => ReservationRendition -> m (HttpResult ())

The presenter of the video goes on to make a concrete version of the postReservation function called postReservationIO in order to eliminate the typeclass constraints. A broken version of the postReservationIO function is written to demonstrate that it cannot just make use of the postReservation function because the IO type returned by the postReservationIO function is not an instance of the MonadReader typeclass.

We are then told that in order to eliminate the MonadReader constraint from the postReservationIO function we need to make use of the runReaderT function which is where the video loses me.

At about 15:00, the postReservationIO function is refactored to look like this

postReservationIO :: ReservationRendition -> IO (Httpresult ())
postReservationIO req = runReaderT (postReservation req) connStr

The runReaderT function has a type signature of ReaderT k r m a -> r -> m a which I'm reading as a function that takes some concrete ReaderT type and some value of type r (the connection string in our case) and it'll give you back some monad of type m a.

In the postReservationIO implementation we are passing (postReservation req) as the first argument to the runReaderT function. (postReservation req) has the type

(MonadReader ConnectionString m, MonadIO m) => m (HttpResult ()) 

which as far as I can tell is not a ReaderT so I'm struggling to understand how this works.

Can anyone explain how we have made the jump from something of type (MonadReader ConnectionString m, MonadIO m) => m (HttpResult ()) to ReaderT k r m a in order to eliminate the MonadReader constraint?

1

1 Answers

6
votes

The m in postReservations type gets instantiated to ReaderT * ConnectionString IO (HttpResult ()), which is an instance of both MonadReader ConnectionString and MonadIO.

Note that ReaderT is only explicitly mentioned through runReaderT. It's that function that demands its argument to be a concrete ReaderT instead of an arbitrary MonadReader ConnectionString.

Edit:

As @Benjamin Hodgson points out, the underlying mechanisms are that of return type polymorphism, or more generally unification.

So, when the body of postReservationIO is type-checked, this is roughly what happens:

-- What we know, because we already type-checked them (this is necessary information about free variables):
runReaderT                               :: ReaderT k r m a -> r -> m a
postReservation req                      :: (MonadReader ConnectionString m', MonadIO m') => m' (HttpResult ())
connStr                                  :: ConnectionString

-- What we want to check
runReaderT (postReservation req) connStr :: IO (HttpResult ())

-- Unifying `runReaderT` with its arguments results in the following constraints:
-- First argument
ReaderT k r m a ~ (MonadReader ConnectionString m', MonadIO m') => m' (HttpResult ())
-- Second argument
r ~ ConnectionString
-- Return type
m (HttpResult ()) ~ IO (HttpResult ())

Read ~ as 'has to unify with'. E.g., the fact that the second argument to runReaderT is a ConnectionString necessitates that the type variable r unifies with ConnectionString.

The constraint ReaderT k r m a ~ (MonadReader ConnectionString m', MonadIO m') => m' (HttpResult ()) is what I was alluding to earlier. This is what instantiates m' to ReaderT * ConnectionString m, which is further instantiated to ReaderT * ConnectionString IO as a result of the the last constraint.

It is only after all type variable constraints are satisfied that GHC checks that ReaderT * ConnectionString IO satisfies MonadReader ConnectionString and MonadIO, which indeed it does.

If this wasn't the case, e.g. when postReservation :: (MonadLogger m, MonadIO m) => ReservationRendition -> m (HttpResult ()), then the compiler wouldn't be able to find the instance MonadLogger (ReaderT * ConnectionString IO) and complain.