Couldn't match type `m` with `ReaderC`
AlistairB opened this issue · comments
Hi
Looking for help on a compilation issue I am getting. Possibly related to #253 . I'm not an expert Haskeller btw 😅
My ultimate goal is to integrate a fused effects effect stack into a servant Handler
. I have done something similar with mtl
in the past. If I can write a function from the effect stack to a Handler a
, then you can use hoistServer
and it should all work.
However, I am struggling to write the function in the case of fused effects.
appToHandler ::
( Has Trace sig m,
Has (Reader String) sig m,
Has (Error AppError) sig m
) =>
String ->
m a ->
Handler a
appToHandler envString app =
app
& (runReader @String envString)
& runError
& runTrace
& runM
& toHandler
toHandler :: IO (Either AppError a) -> Handler a
toHandler = undefined
fails with:
• Couldn't match type ‘m’
with ‘ReaderC
String (ErrorC AppError (TraceC (Control.Carrier.Lift.LiftC IO)))’
‘m’ is a rigid type variable bound by
the type signature for:
appToHandler :: forall (sig :: (* -> *) -> * -> *) (m :: * -> *) a.
(Has Trace sig m, Has (Reader String) sig m,
Has (Error AppError) sig m) =>
String -> m a -> Handler a
at /tmp/haskell-lsp8148/Application.hs-00637-2872700011475582556.hs:(50,1)-(57,11)
Expected type: m a
-> ErrorC AppError (TraceC (Control.Carrier.Lift.LiftC IO)) a
Actual type: ReaderC
String (ErrorC AppError (TraceC (Control.Carrier.Lift.LiftC IO))) a
-> ErrorC AppError (TraceC (Control.Carrier.Lift.LiftC IO)) a
• In the second argument of ‘(&)’, namely
‘(runReader @String envString)’
In the first argument of ‘(&)’, namely
‘app & (runReader @String envString)’
In the first argument of ‘(&)’, namely
‘app & (runReader @String envString) & runError’
• Relevant bindings include
app :: m a
(bound at /tmp/haskell-lsp8148/Application.hs-00637-2872700011475582556.hs:58:24)
appToHandler :: String -> m a -> Handler a
(bound at /tmp/haskell-lsp8148/Application.hs-00637-2872700011475582556.hs:58:1)
It does work when not passing in the app
, but referring to a local definition:
application ::
( Has Trace sig m,
Has (Reader String) sig m,
Has (Error AppError) sig m
) =>
m a
application = undefined
appToHandler2 :: String -> Handler a
appToHandler2 envString =
application
& (runReader envString)
& runError
& runTrace
& runM
& toHandler
Which makes my RankNTypes
sense tingle.. . The only way I have been able to get something to work, but seems very non-optimal is:
-- AllowAmbiguousTypes must be enabled
appToHandler ::
( Has Trace sig m,
Has (Reader String) sig m,
Has (Error AppError) sig m
) =>
String ->
(forall m. m a) ->
Handler a
I've also tried throwing in Algebra sig m
constraints which I hoped via fundeps, might make it unambiguous, but no luck.
Um, any pointers on what the best way to solve this is? Am I doing something fundamentally silly?
Effect handlers like runReader
are the point where you selecting a concrete implementation of an effect: runReader :: r -> ReaderC r m a -> m a
. So actions (like application
) should be written in terms of Has
constraints, but handlers (like appToHandler
) should be written in terms of the concrete carrier types they handle. So in this case rather than:
appToHandler ::
( Has Trace sig m,
Has (Reader String) sig m,
Has (Error AppError) sig m
) =>
String ->
m a ->
Handler a
you’d want a signature like
appToHandler ::
String ->
(ReaderC String (ErrorC AppError (TraceC IO))) a ->
Handler a
You’re right that you could also employ RankNTypes
(cf #240 (comment)), but a) this is much more complex when you don’t strictly need to, and b) it can come at a cost in efficiency if the compiler isn’t able to specialize and inline the algebras using the concrete handlers you’ve selected.
Thanks @robrix ! 🙇♂️
I think I see now. That makes sense. I'll see if I can wire it all up. To make it happy I needed to do exactly:
appToHandler ::
String ->
(ReaderC String (ErrorC AppError (TraceC (LiftC IO)))) a ->
Handler a
appToHandler envString app =
app
& (runReader @String envString)
& runError @AppError
& runTrace
& runM
& toHandler
@AlistairB: ah indeed—you can drop LiftC
if you also drop runM
, which is usually unnecessary these days. (Because IO
has an Algebra
instance.)
Ah nice, that works :)
For people that end up here in the future, here's a simple working example:
import Servant
import Control.Carrier.Reader hiding (run)
import Control.Carrier.Lift hiding (run)
type ExampleRoute = "example"
:> QueryParam "code" Text
:> QueryParam "name" Text
:> QueryParam "enrollment" Bool
:> Get '[JSON] Text
type Api = ExampleRoute
data AppEnv = AppEnv {
secret :: !(Maybe Text),
exampleList :: ![Text]
}
type Web sig m = (Has (Reader AppEnv) sig m)
type WebConcrete = (ReaderC AppEnv (LiftC Handler))
exampleHandler ::
(Web sig m) =>
Maybe Text ->
Maybe Text ->
Maybe Bool ->
m Text
exampleHandler _ _ _ = do
appEnv <- ask @AppEnv
... do some stuff
server :: ServerT Api WebConcrete
server = exampleHandler
apiProxy :: Proxy Api
apiProxy = Proxy
appToHandler :: AppEnv -> WebConcrete a -> Handler a
appToHandler appEnv = runM . runReader @AppEnv appEnv
app :: AppEnv -> Application
app appEnv = serve apiProxy $ hoistServer apiProxy (appToHandler appEnv) server
startApp :: AppEnv -> IO ()
startApp appEnv = run 8080 (app appEnv)
main :: IO ()
main = do
startApp (AppEnv Nothing [])