haskell-effectful/effectful

How to return a conduit from an effect?

endgame opened this issue · 3 comments

I have been trying to write an effect for the functions in ftp-client-conduit:

data Ftp :: Effect where
  -- Among others
  Retr :: String -> Ftp m (ConduitT Void ByteString m ())

$(makeEffect ''Ftp)

runFtpIO ::
  ( IOE :> es,
    Reader Ftp.Handle :> es,
    Resource :> es
  ) =>
  Eff (Ftp ': es) a ->
  Eff es a
runFtpIO = interpret $ \_ -> \case
  Retr s -> do
    h <- ask
    pure $ Ftp.retr h s

This fails with the following error:

 error:
    • Could not deduce (IOE :> localEs)
        arising from a use of ‘Ftp.retr’
      from the context: (IOE :> es, Reader Ftp.Handle :> es,
                         Resource :> es)
        bound by the type signature for:
                   runFtpIO :: forall (es :: [Effect]) a.
                               (IOE :> es, Reader Ftp.Handle :> es, Resource :> es) =>
                               Eff (Ftp : es) a -> Eff es a
        at <location>
      or from: (HasCallStack, Ftp :> localEs)
        bound by a type expected by the context:
                   EffectHandler Ftp es
        at <location>
      or from: a1 ~ ConduitT Void ByteString (Eff localEs) ()
        bound by a pattern with constructor:
                   Retr :: forall (m :: * -> *).
                           String -> Ftp m (ConduitT Void ByteString m ()),
                 in a case alternative
        at <location>
    • In the second argument of ‘($)’, namely ‘Ftp.retr h ss’
      In a stmt of a 'do' block: pure $ Ftp.retr h ss
      In the expression:
        do h <- ask
           pure $ Ftp.retr h ss
   |
xx |     pure $ Ftp.retr h ss
   |            ^^^^^^^^

It looks to me like we cannot know what's going inside the monad that the ConduitT is applied to, and since @isovector has hit the same problem I think it looks pretty fundamental to the way these effect libraries work.

In effectful's case, it seems like I can get it to compile by being specific about the monad inside the conduit:

data Ftp :: Effect where
   -- I think I like this the best, as we can lift into whatever we need
  Retr :: String -> Ftp m (ConduitT Void ByteString ResIO ())

  -- Constrain the monad
  Retr' :: (MonadIO n, MonadResource n) => String -> Ftp m (ConduitT Void ByteString n ())

  -- Constrain the monad to be an Eff
  Retr'' :: (IOE :> es, Resource :> es) => String -> Ftp m (ConduitT Void ByteString (Eff es) ())

Do you have any alternate suggestions?

Hmm. This should work:

runFtpIO ::
  ( IOE :> es,
    Reader Handle :> es,
    Resource :> es
  ) =>
  Eff (Ftp ': es) a ->
  Eff es a
runFtpIO = interpret $ \env -> \case
  Retr s -> do
    h <- ask
    localLiftUnlift env SeqUnlift $ \lift _unlift -> do
      pure . transPipe lift $ Ftp.retr h s

It looks like this use case warrants addition of localSeqLift and localLift to Effectful.Dynamic.Dispatch so that the unlifting function doesn't have to be redundantly created.

EDIT: with #109:

runFtpIO ::
  ( IOE :> es,
    Reader Handle :> es,
    Resource :> es
  ) =>
  Eff (Ftp ': es) a ->
  Eff es a
runFtpIO = interpret $ \env -> \case
  Retr s -> do
    h <- ask
    localSeqLift env $ \lift -> do
      pure . transPipe lift $ Ftp.retr h s

Thanks for the quick response, I'd been beating my head against this all day because I somehow missed localLiftUnlift. I put the connection to the FTP server into this handler instead of creating a Reader Ftp.Handle effect, and came up with this:

runFtpIO ::
  forall es a.
  (IOE :> es, Resource :> es) =>
  String ->
  Int ->
  Eff (Ftp ': es) a ->
  Eff es a
runFtpIO host port m = Ftp.withFTP host port $ \h _ -> interpret (handler h) m
  where
    handler :: Ftp.Handle -> EffectHandler Ftp es
    handler h env ftp = localLiftUnlift env SeqUnlift $ \lift _ -> case ftp of
      List ss -> pure . transPipe lift $ Ftp.list h ss
      Retr s -> pure . transPipe lift $ Ftp.retr h s

I'm going to close this now, since the thing I want is possible with current effectful, but I look forward to a release containing #109.

FYI 2.2.1.0 is released 🙂