Let's assume I have an environment record like this:
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
type RIO env a = ReaderT env IO a
data Env = Env
{ foo :: Int -> String -> RIO Env (),
bar :: Int -> RIO Env Int
}
env :: Env
env =
Env
{ foo = \_ _ -> do
liftIO $ putStrLn "foo",
bar = \_ -> do
liftIO $ putStrLn "bar"
return 5
}
The functions stored in the environment might have different number of arguments, but they will always produce values in the RIO Env
monad, that is, in a ReaderT
over IO
parameterized by the environment itself.
I would like to have a succinct way of invoking these functions while inside the RIO Env
monad.
I could write something like this call
function:
import Control.Monad.Reader
call :: MonadReader env m => (env -> f) -> (f -> m r) -> m r
call getter execute = do
f <- asks getter
execute f
And use it like this (possibly in combination with -XBlockArguments
):
example1 :: RIO Env ()
example1 = call foo $ \f -> f 0 "fooarg"
But, ideally, I would like to have a version of call
which allowed the following more direct syntax, and still worked for functions with a different number of parameters:
example2 :: RIO Env ()
example2 = call foo 0 "fooarg"
example3 :: RIO Env Int
example3 = call bar 3
Is that possible?
From the two examples, we can guess that
call
would have type(Env -> r) -> r
.Put that in a type class, and consider two cases,
r
is an arrowa -> r'
, orr
is anRIO Env r'
. Implementing variadics with type classes is generally frowned upon because of how fragile they are, but it works well here because theRIO
type provides a natural base case, and everything is directed by the types of the accessors (so type inference isn't in the way).