1

I'm trying to marry the approach given at http://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/ (section titled "Typeclasses can emulate effects") with some sort of homegrown reader monad.

The overall problem I'm trying to solve is to avoid passing around a configuration variable to almost ever function in my app. And the reason I can't use a ReaderT is because a lot of my functions are in SqlPersistT, which itself uses a ReaderT internally. The other reason is to learn all this mental gymnastics better.

My two questions are given as comments in the code below. Reproducing them here as well:

  • What is the most appropriate way to define NwMonad?
  • Consequently, how do define NwMonad as an instance of HasNwConfig? How to write the function body of askNwConfig?
  • How do I finally call runNwMonad? What will be its arguments?

Here's the code:

data NwConfig = NwConfig {
  _googleClientId :: T.Text,
  _googleClientSecret :: T.Text,
  _tgramBotToken :: String,
  _aria2Command :: String,
  _aria2DownloadDir :: String
  }
$(makeLenses ''NwConfig)

instance Default NwConfig where
  def = NwConfig{}

class MonadIO m => HasNwConfig m where
  askNwConfig :: m NwConfig

startAria2 :: (HasNwConfig m) => m Sytem.Process.ProcessHandle
  cfg <- askNwConfig
  (_, _, _, processHandle) <- createProcess $ proc (cfg ^. aria2Command) []
return processHandle


-- QUESTION: Is this correct?
data NwMonad a = NwMonad{runNwMonad :: (NwConfig -> IO a)}
               deriving (Functor, Applicative, Monad, MonadIO)

-- Or is this the way to do it?
data NwMonad a = NwMonad{runNwMonad :: IO a, nwConfig :: NwConfig}
               deriving (Functor, Applicative, Monad, MonadIO)

instance HasNwConfig NwMonad where
  askNwConfig = return . nwConfig -- QUESTION: How to write this?

main :: IO ()
main = do
  [cId, cSecret, botToken] <- sequence [getEnv "GOOGLE_CLIENT_ID", getEnv "GOOGLE_CLIENT_SECRET", getEnv "TELEGRAM_TOKEN"]
  let cfg = (def :: NwConfig)
        & googleClientId .~ (T.pack cId)
        & googleClientSecret .~ (T.pack cSecret)
        & tgramBotToken .~ botToken
        & aria2Command .~ "/Users/saurabhnanda/projects/nightwatch/aria2-1.19.3/bin/aria2c"
  -- QUESTION: How do I use this now?
  runNwMonad $ (?????) $ startAria2
Saurabh Nanda
  • 6,373
  • 5
  • 31
  • 60

1 Answers1

1

Here's some code which shows how to work with multiple Reader environments in the same transformer stack. Here BaseMonad is like your SqlPersistT:

import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.IO.Class

type BaseMonad = ReaderT String IO

type NwMonad = ReaderT Int BaseMonad

askString :: NwMonad String
askString =  lift ask

askInt :: NwMonad Int
askInt = ask

startAria :: NwMonad ()
startAria = do
  i <- askInt
  s <- askString
  liftIO $ putStrLn $ "i: " ++ show i ++ " s: " ++ s

main = do
  let cfg = 10       -- i.e. your google client data
      s = "asd"      -- whatever is needed for SqlPersistT
  runReaderT (runReaderT startAria cfg) s

Here's some code using the SqlPersisT type and runSqlConn:

import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.IO.Class
import Database.Persist.Sql

data Config = Config { _clientId :: String }

type BaseMonad = SqlPersistT IO

type NwMonad = ReaderT Config BaseMonad

askBackend:: NwMonad SqlBackend
askBackend =  lift ask

askConfig :: NwMonad Config
askConfig = ask

startAria :: NwMonad ()
startAria = do
  cfg <- askConfig
  liftIO $ putStrLn $ "client id: " ++ (_clientId cfg)

main = do
  let cfg = Config "foobar"
      backend = undefined :: SqlBackend -- however you get this
      sqlComputation = runReaderT startAria cfg :: SqlPersistT IO ()
  runSqlConn sqlComputation backend :: IO ()

Update

The type of the environment doesn't matter.

import Control.Monad.Reader
import Control.Monad.IO.Class

type Level1  =  ReaderT Int IO
type Level2  =  ReaderT Int Level1
type Level3  =  ReaderT Int Level2

ask3 :: Level3 Int
ask3 = ask

ask2 :: Level3 Int
ask2 =  lift ask

ask1 :: Level3 Int
ask1 =  lift $ lift $ ask

doit :: Level3 ()
doit = do
  r1 <- ask1
  r2 <- ask2
  r3 <- ask3
  liftIO $ print (r1, r2, r3)

main = do
  runReaderT (runReaderT (runReaderT doit 333) 222) 111
ErikR
  • 51,541
  • 9
  • 73
  • 124
  • Thanks @ErikR. What if I want to do this *without* using the regular ReaderT monad? Is my approach completely unworkable? Any help with completing the approach that I was following above? – Saurabh Nanda Jun 26 '16 at 11:05
  • Conceptually does this work on the basis of which type of environment is being asked? If it's a `String` let the outer modad (or is it inner?) handle it, if it's `Int` let the inner monad (or is it outer?) handle it. It seems like this should be categorised as an anti-pattern or code-smell. Something doesn't seem right to me over here. What do you think about using the approach given at http://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/ ? – Saurabh Nanda Jun 26 '16 at 11:12
  • Answer updated. You can create your own monad if you want, but you don't have to just because you want to create a ReaderT on top of another ReaderT. – ErikR Jun 26 '16 at 14:23
  • Thanks ErikR for clarifying again. While this gives me a working solution, it doesn't help me understand how this is really working. And something about nested ReaderT's along with chained lift's don't make this seem like an elegant solution. Can't one "extend" a single monad (probably using typeclasses) to "add more" functionality to it? (Is this even making sense?) – Saurabh Nanda Jun 26 '16 at 15:56
  • I think I've answered your original question. If you need more help understanding how `lift` works (in the `mtl` library) please open up a new question. – ErikR Jun 26 '16 at 16:57
  • For what it’s worth, @SaurabhNanda, I am the author of that blog post, and our app’s monad looks basically like `newtype AppM a = AppM (ReaderT Config IO a)`, where `Config` is a record containing our application’s configuration, and we derive `MonadIO` and `MonadReader` using GND. Then, in the typeclass implementation, we just use `asks` and `liftIO` to implement the individual typeclasses, just like in this answer. – Alexis King Jun 26 '16 at 17:34
  • Thanks for helping me out! – Saurabh Nanda Jun 26 '16 at 20:23