haskell-servant / servant

Servant is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.83k stars 413 forks source link

How to get access to client functions when passing params to hoistClient #1019

Open onepunchtech opened 6 years ago

onepunchtech commented 6 years ago

following the documentation here: https://haskell-servant.readthedocs.io/en/stable/tutorial/Client.html#changing-the-monad-the-client-functions-live-in

you end up with

-- our conversion function has type: forall a. ClientM a -> IO a
-- the result has type:
-- Client IO HoistClientAPI = IO Int :<|> (Int -> IO Int)
getClients :: ClientEnv -> Client IO HoistClientAPI
getClients clientEnv
  = hoistClient hoistClientAPI
                ( fmap (either (error . show) id)
                . flip runClientM clientEnv
                )
                (client hoistClientAPI)

How do you access the client functions? I tried something like this, but I get a parse error.

 (clientEnv (getInt :<|> postInt)) = hoistClient hoistClientAPI
                ( fmap (either (error . show) id)
                . flip runClientM clientEnv
                )
                (client hoistClientAPI)
alpmestan commented 6 years ago

You just need to do:

getInt :<|> postInt = hoistClient hoistClientAPI ( ... ) (client hoistClientAPI)

It's just the same as usual, really. Except that the client functions got some "post-processing" applied to them, to bring their result in IO. Let me know if that's clear enough or if you still have trouble understanding any of this.

onepunchtech commented 6 years ago

but how do you pass in clientEnv? The examples I've seen people will use unsafePerformIO, but what if I wanted a cleaner way to pass it in. It seams like it would be hard to use clientEnv as a free variable and also keep these functions in a separate reusable module.

alpmestan commented 6 years ago

Well, it's not clear to me what you're trying to accomplish exactly. ClientM is already a ReaderT ClientEnv (...), so all the client functions are already "waiting to be given a ClientEnv", so to speak. What the example from the tutorial quoted above accomplishes is to bring them all back to IO, by supplying a fixed client environment. You however apparently don't want to do that, so why do you need hoistClient ? Could you perhaps share the actual example your working with, or a smaller, simpler one that mimics it?

phadej commented 6 years ago

@alpmestan

getInt :: ClientEnv -> Client IO GetIntEndpoint
postInt :: ClientEnv -> Client IO PostIntendpoint

I know that you can get (i.e. ClientEnv argument last)

getInt :: Client ((->) ClientEnv) GetIntEndpoint
postInt :: Client ((->) ClientEnv) PostIntendpoint

but if one really insist to have it first? I don't really know

alpmestan commented 6 years ago

Oh, hmm, right, I don't know of a solution to flip the ClientEnv around like this.

onepunchtech commented 6 years ago

Actually that makes sense about just leaving it in ClientM as it is a reader monad. The docs said that it is rarely if ever the right monad to use though. So I was confused as to what the best way to organize code would be. I guess I was just trying to export simple client functions in the IO monad, and instead of having the reader monad provide the client env you could do it like @phadej said above.

alpmestan commented 6 years ago

For the record, what we had in mind when saying "it is rarely the right monad" is that the client functions are rarely used in code that lives in ClientM, but instead in some app-specific monad or something.