4

I would like to embed ReaderT into another monad transformer. How do I do this? The example below uses Scotty but I think it would be the same with any other monad.

{-# LANGUAGE OverloadedStrings #-}

import qualified Web.Scotty
import Web.Scotty.Trans

import Data.Text.Lazy
import Control.Monad.IO.Class (liftIO)

import Control.Monad.Trans.Reader
import Control.Monad.Trans

data Config = Config Text

main :: IO ()
main = do
    let config = Config "Hello World"
    -- how to I make this line work?
    scottyT 3000 id id routes

routes :: ScottyT Text (ReaderT Config IO) ()
routes = do
    get "/" info

info :: ActionT Text (ReaderT Config IO) ()
info = do
    -- this part seems like it works!
    Config message <- lift ask
    text $ "Info: " `append` message

This errors on the line scottyT 3000 id id routes, because scottyT expects a ScottyT Text IO (). How do I make this work? Here are the current errors:

Server.hs: line 24, column 24:
  Couldn't match type `ReaderT Config IO' with `IO'
    Expected type: ScottyT Text IO ()
      Actual type: ScottyT Text (ReaderT Config IO) ()
Sean Clark Hess
  • 15,859
  • 12
  • 52
  • 100
  • Well, this works but I'm not sure it's right: `flip runReaderT config $ scottyT 3000 id (flip runReaderT config) routes`. I don't think it's correct considering `runReaderT` is needed twice, but I don't know enough about scotty to know why the `scottyT` function requires that argument – bheklilr Sep 26 '14 at 18:14
  • Could you just change the order of the "Transformes stack" i.e. `ReaderT Config (ScottyT Text IO) ()`? – felix-eku Sep 26 '14 at 18:16
  • Based off of [this](http://stackoverflow.com/a/23190718/839246) answer, you can do `scottyT 3000 (flip runReaderT) (flip runReaderT) routes, and that seems to be the way other people have done it. – bheklilr Sep 26 '14 at 18:19
  • @chaosmasttter Unfortunately, no. The `get` function has the type `(ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()`, instead of the more general `(ScottyError e, MonadIO m, MonadAction a, MonadScotty s) => RoutePattern -> a e m () -> s e m ()` – bheklilr Sep 26 '14 at 18:21
  • @chaosmasttter I don't want to. I want to be able to use `Scotty` stuff without lifting. – Sean Clark Hess Sep 26 '14 at 18:24

1 Answers1

1

You have to change the arguments you've supplied as id to be ones that have the type forall a. m a -> n a and m Response -> IO Response respectively. Why? I don't know, but the example I found here shows someone running it similar to

main = do
    let config = Config "Hello, world"
        runner = flip runReaderT config
    scottyT 3000 runner runner routes

I've tested it, and it at least works. Whether or not this is best practices is unknown to me. If someone has a better method, feel free to post it.

Community
  • 1
  • 1
bheklilr
  • 53,530
  • 6
  • 107
  • 163
  • How would it look with a more understandable monad? It looks like Scotty added some magic to enable this, but what if they hadn't? would it still be possible? – Sean Clark Hess Sep 26 '14 at 18:38
  • @SeanClarkHess What I've normally seen is that you'd have something like `scottyT :: (ScottyError e, MonadIO m) => Port -> ScottyT e m () -> m ()`, then you could have `flip runReaderT config $ scottyT 3000 routes` instead by only running the `ScottyT` layer. It seems that scotty requires some more advanced behavior for executing the actions, I find it hard to believe that they'd need these complex extra parameters just from poor API design. Personally, I would prefer being able to run the `ReaderT` monad first, leaving a `ScottyM` monad as my base to just run with `scotty`, but that's just me – bheklilr Sep 26 '14 at 18:44
  • 2
    For Scotty ≥ 0.10, `scottyT` no longer takes the “Run monad 'm' into monad 'n', called once at 'ScottyT' level” parameter, so the last line should be `scottyT 3000 runner routes`. – beta Jul 10 '15 at 08:17