Closed boj closed 5 years ago
Consider an interpreter (runM .@ runMessageReaderIO con) . runState s
. Here, we have a State
effect in scope inside of any calls to waitMessage
. And the question is, "how should that state get threaded back into the original computation?"
Admittedly, this error you've reported is not very concise. What it's trying to tell you by saying that you gave it a ()
but it wants a f ()
is that you've discarded statefulness here. Any changes you made to the State
effect (or any other effect above it) haven't been propagated outwards.
In the common case of returning an m ()
, you can simply bite the bullet and just return getInitialStateT
--- in effect saying "I acknowledge that I'm ignoring the state changes!". Such a thing will "work," but might have surprising consequences.
It's worth keeping in mind that this is the exact same limitation that MonadBaseControl
and MonadUnliftIO
have; except that in polysemy
the typesystem lets you know when you're doing something sketchy --- rather than just invisibly deleting your effects!
An alternative is to write your effect as being polymorphic in its return type:
WaitMessage :: Integer -> ByteString -> (ByteString -> m a) -> MessageReader m a
which often seems like a more "honest" type to return. Whether you can get away with this type in your interpreter is another question --- if its running in the same thread, then yes, otherwise probably not.
I considered that type signature, but still get the same error with it.
Whether you can get away with this type in your interpreter is another question
Looks like no. pubSub
itself discards whatever type you wanted to run inside of it. The documentation suggests why: "It should be noted that Redis Pub/Sub by its nature is asynchronous"
Hmm. How would one use such a function in polysemy
? I am still trying to build up a good picture of how to build on freer monads for applications.
You just bite the bullet and write your interpreter like this:
runMessageReaderIO
:: forall r a.
Member (Lift IO) r
=> Connection
-> (forall x. Sem r x -> IO x)
-> Sem (MessageReader ': r) a
-> Sem r a
runMessageReaderIO conn r = interpretH $ \case
WaitMessage mid dom f -> do
is <- getInitialStateT
f' <- bindT f
sendM $ runRedis conn $ pubSub (subscribe [ mkIdKey dom mid ]) $ \msg -> do
r .@ runMessageReaderIO conn $ f' (msgMessage msg <$ is)
pure (unsubscribe [ msgChannel msg ])
getInitialStateT
Ok, with the callback type back to m ()
this works. I barely understand why I would have to do this myself, and am slightly worried having to explain this to my team. I suppose more experience with the library will help.
Thanks very much for your assistance!
I barely understand why I would have to do this myself,
Out of genuine curiosity, how would you like this problem to be solved?
PS If you'd like to schedule a call and hash this out in a higher-bandwidth setting, I'd be more than happy!
@isovector You know, a call would be great. Do you have any preference as to technology and/or time?
I like google hangouts, where my username is sandy.g.maguire@gmail.com
. I'm in UTC -5, and any day in the 12pm-5pm range is fine by me.
Low bandwidth version (maybe we can chat at some point, I pinged you via mojobojo@gmail.com
):
Out of genuine curiosity, how would you like this problem to be solved?
I honestly don't know. There's still a lot of mechanics in Haskell I don't understand (for example, you mentioned MonadBaseControl
and MonadUnliftIO
- if I understand correctly they reverse-lift IO things, but still have a hard time conceptualizing when/how to use them).
It seems obvious that at some point one will have to write an effect which uses Tactical
, I've come across this twice already with regards to callback functions, this last one slightly more confusing because it was an async call. Maybe a blog post which explains what passing a runner in the form (forall x. Sem r x -> IO x)
does, what getInitialStateT
does and why it turns everything into a Functor
, and how it interacts with runT
and bindT
? How this internal state is tracked, how it applies to functions and parameters you call in a different monadic context, and how the interaction of it all works?
I suppose I'm imagining someone new to Haskell who asks the question we've heard numerous times, "How do you structure a Haskell app? Why would I pick polysemy
over mtl
over x
?", and then seeing them get stuck on the same thing I have (a somewhat seasoned Haskeller, but after using it for 5 years still realize there is a lot I don't understand - the internals of your library are magic to me).
I wish I had a better answer, but most of this stems from my own ignorance.
Similar to last time I still feel like I am not grokking how to use Tactics (or how to track the proper monadic state in my head, which seems like an unnecessary burden).
Playing with the
hedis
library: