ReaderT: ask for multiple functions

Hello! I would like to have a Reader which gives me multiple functions. For example, if I have two possible databases, it does not make sense to read from database A and delete from the other.
Here is a made up example with 1 function

initA :: String -> Effect Unit
initA param = log $ "init A " <> param

main :: Effect Unit
main = runReader go initA

go :: ReaderT (String -> Effect Unit) Identity (Effect Unit)
go = do
  result <- ask
  pure $ result "works"

“init A works”

2 functions:


getFromA :: String -> Effect String
getFromA param = pure $ "from A " <> param

initB :: String -> Effect Unit
initB param = log $ "init B " <> param

getFromB :: String -> Effect String
getFromB param = pure $ "from B " <> param

type System
  = { init :: String -> Effect Unit, get :: String -> Effect String }

systemA :: System
systemA = { init: initA, get: getFromA }

systemB :: System
systemB = { init: initB, get: getFromB }

main' :: Effect Unit
main' = (runReader go' systemA) >>= log

go' :: ReaderT (System) Identity (Effect String)
go' = do
  result <- ask -- <-Error
  result.init "works"
  pure $ result.get "works"

Could not match type Effect with type ReaderT { get :: String -> Effect String, init :: String -> Effect Unit} Identity

Can somebody explain why the first one works, the last one doesn’t?

Shouldn’t go' have this type signature?

go' :: forall m. MonadReader System m => m String

The problem here is not the line you pointed out, but the following one.

result.init gives you an Effect Unit, but your function go' needs type ReaderT (System) Identity (Effect String).

If you add pure just like you did on the last line, and explicitly discard the result with void, your code will compile:

void $ pure $ result.init "works"

At least that’s one of the possible solutions. I hope it helps. :slight_smile:

I think you want lift or liftEffect.

Thank you all very much for your responses! :+1:

  • go' :: forall m. MonadReader System m => m String did not work for me.
  • void ... did work but for example
go_simple :: Effect String
go_simple = do
  systemA.init "works"
  s1 <- systemA.get "one"
  s2 <- systemA.get ("one" <> s1)
  pure s2

works and the Reader solution should look and behave similar. Switching to runReaderT, changing the transformer stack and lifting every function does the trick.

main' :: Effect Unit
main' = (runReaderT go' systemA) >>= log

go' :: ReaderT (System) Effect (String)
go' = do
  result <- ask
  liftEffect $ result.init "works"
  s1 <- liftEffect $ result.get "one"
  s2 <- liftEffect $ result.get ("one" <> s1)
  pure $ s2

FWIW you don’t necessarily have to lift each individual bind.

go' :: ReaderT (System) Effect (String)
go' = do
  result <- ask
  liftEffect do 
    result.init "works"
    s1 <- result.get "one"
    s2 <- result.get ("one" <> s1)
    pure s2
1 Like

Now that you’re using liftEffect, the type signature mentioned earlier will probably work if you also take Effect into account:

go' :: forall m. MonadReader System m => MonadEffect m => m String

If liftEffect works only with Effect and lift works with every transformer, I should stick with lift.
Lifting do to see different levels is nice. I have tried it with a longer stack:

go' :: ReaderT System (ExceptT String Effect) String
go' = do
  result <- ask
  lift $ do
    s1 <- lift $ do
      result.init "works"
      result.get "one"
    result.getEE $ s1

Unfortunately purty changes it:

go' :: ReaderT System (ExceptT String Effect) String
go' = do
  result <- ask
  lift
    $ do
        s1 <-
          lift
            $ do
                result.init "works"
                result.get "one"
        result.getEE $ s1

Just fyi, but you don’t need a $ between lift/liftEffect and do

-- works
lift $ do

-- also works
lift do
1 Like

Thanks a lot. I realized if I make a mistake at calling the asked functions, it is hard to spot the error because it only points to the line of the ask statement.

I have tried to make a new monad with newtype and keep ReaderT only for config values. I have integrated ExceptT because I would like to use simple functions (maybe with effects) which return either error string or success something. They should be combined like in a try … catch block and go to catch at the first Left.

type Env
  = { url :: String
    }

newtype AppM a
  = AppM (ReaderT Env (ExceptT String Effect) a)

runAppM :: Env -> AppM String -> Effect Unit
--runAppM :: Env -> AppM ~> Effect --<-- Not sure about that!
runAppM env (AppM m) =
  runExceptT (runReaderT m env)
    >>= case _ of
        Left e -> log e
        Right a -> log a

derive newtype instance functorAppM :: Functor AppM
derive newtype instance applyAppM :: Apply AppM
derive newtype instance applicativeAppM :: Applicative AppM
derive newtype instance bindAppM :: Bind AppM
derive newtype instance monadAppM :: Monad AppM
derive newtype instance monadEffectAppM :: MonadEffect AppM

class
  Monad m <= ManageUser m where
  init :: String -> m Unit
  get :: String -> m String
  getEE :: String -> m String

instance manageUserAppM :: ManageUser AppM where
  init param = liftEffect $ log $ "init A " <> param
  get param = pure $ "from A " <> param
  getEE input = lift $ except $ if input == "from A http://foo" then Right "success" else Left "failure"
 --<--Error Could not match type t0 (ExceptT String t3) with type AppM

instance monadAskAppM :: TypeEquals e Env => MonadAsk e AppM where
  ask = AppM $ asks from

program ::
  forall m a.
  MonadAsk Env m =>
  ManageUser m =>
  m a
program = do
  config <- ask
  init config.url
  s1 <- get "one"
  s2 <- getEE s1
  getEE ("fail please" <> s2)
  lift $ lift $ log "no no no"

I don’t understand the problem with the getEE line. if ... returns an Either, except returns ExceptT, to complete AppM the ReaderT is missing, which I get with lift.

ok, turns out

  getEE input = AppM $ lift $ ...
...
program ::
  forall m.
  MonadAsk Env m =>
  ManageUser m =>
  m String
program = do
  config <- ask
  init config.url
  s1 <- get config.url
  s2 <- getEE s1
  s3 <- getEE ("fail please" <> s2)
  getEE ("no no no" <> s3)

works.

1 Like