fused-effects / fused-effects

A fast, flexible, fused effect system for Haskell

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 [])