simmsb / calamity

A library for writing discord bots in haskell
https://hackage.haskell.org/package/calamity
MIT License
109 stars 11 forks source link

Question: how invole sendMssage on IO () scope #58

Closed Miezhiko closed 1 year ago

Miezhiko commented 1 year ago

Trying to reply @Text message myVal inside just IO () method

I'm starting some separated polling loop the same time with a bot and want to have ability to call bot functionality such sendMessage etc there too.

    • Couldn't match expected type: IO a0
                  with actual type: Polysemy.Internal.Sem
                                      r0 (Either RestError Message)

as far a I understand I still can do it with polysemy eval?

please provide minimal example

simmsb commented 1 year ago

One way is to some some function like the following:

bindSemToIO :: forall r p a. P.Member (P.Final IO) r => (p -> P.Sem r a) -> P.Sem r (p -> IO (Maybe a))
bindSemToIO m = P.withStrategicToFinal $ do
  istate <- P.getInitialStateS
  m' <- P.bindS m
  ins <- P.getInspectorS
  P.liftS $ pure (\x -> P.inspect ins <$> m' (istate $> x))

This works by capturing the state of the Sem and packaging it into the final monad. (The reason for the Maybe is that a Sem a doesn't always yield an a if an exception effect is in use)

Using this is most likely fine because you can't use plain State effects with calamity anyway, but be aware that if you use this anywhere else local state changes won't escape the IO packaged action and every time you execute the action any local state will be the same as it was when the state was initially captured.

Miezhiko commented 1 year ago

I am very sorry but this looks way too complicated for me to understand this code and usage.

that's how I was trying to use it :-/

import           Control.Applicative
import           Control.Monad

import           Optics
import           Optics.TH

import           Calamity

import           Data.Text           (Text)
import qualified Data.Text           as T

import           Polysemy            (Member)
import qualified Polysemy            as P
import qualified Polysemy.Embed      as P
import qualified Polysemy.Final      as P
import qualified Polysemy.State      as P

bindSemToIO ∷ forall r p a. P.Member (P.Final IO) r
            => (p -> P.Sem r a)
            -> P.Sem r (p -> IO (Maybe a))
bindSemToIO m = P.withStrategicToFinal $ do
  istate  <- P.getInitialStateS
  m'      <- P.bindS m
  ins     <- P.getInspectorS
  P.liftS $ pure (\x -> P.inspect ins <$> m' (istate $> x))

replyBotEff ∷ Snowflake Channel
           -> Snowflake Message
           -> String
           -> IO ()
replyBotEff _chanId mesgId stxt =
  void $ bindSemToIO
       $ void $ reply @Text mesgId (T.pack stxt)
simmsb commented 1 year ago

Ah it's a bit weird to use but here's how it works:

The parameter to the function (p -> P.Sem r a) should be some function that takes a parameter and returns a polysemy action, so something like \(id, text) -> void $ reply id text (which will have the type P.Member ... r => (Snowflake Channel, Text) -> P.Sem r ...

Using this bindSemToIO (\(id, text) -> ...) will get you something with the type P.Member ... r => P.Sem r ((Snowflake Channel, Text) -> IO (Maybe ...))

You can then use that action inside some Sem context to get your (Channel, Text) -> IO (Maybe ...) that you can then pass around wherever you want and use within an IO context.

foo :: P.Sem r ()
foo = do
    act <- bindSemToIO (\(id, text) -> void $ reply id text)
    P.embed . forkIO $ do
        act foo bar

The Sem r (p -> IO (Maybe a)) action serves to collect the state of the Sem and package it up in an IO

Miezhiko commented 1 year ago

still wasn't able to do it, need to convert message from snowflake, many types errors

can't just do

kreply ∷ P.Members '[P.Fail] r
 => (Snowflake Message, T.Text) -> P.Sem r ()
kreply (target, txt) = void $ reply @Text target txt
• Could not deduce (P.Member
                      (DiPolysemy.Di
                         df1-0.4.1:Df1.Types.Level
                         df1-0.4.1:Df1.Types.Path
                         df1-0.4.1:Df1.Types.Message)
                      r)
    arising from a use of ‘reply’
  from the context: P.Members '[P.Fail] r
    bound by the type signature for:
               kreply :: forall (r :: P.EffectRow).
                         P.Members '[P.Fail] r =>
                         (Snowflake Message, Text) -> P.Sem r ()
simmsb commented 1 year ago

Oops I forgot the type of reply You can just pass Message instead of Snowflake Message

The type error here is because you're missing an effect reply needs from the effects in P.Members '[P.Fail] r It's not possible to get hold of a (Message, Text) -> IO () without first having the bot start up, as the token and other state used by reply won't be available until after the bot starts.

If you're starting up some message bus consumer and need to send messages from it, you could start the loop from within the bot context like this:

runBotIO (BotToken ...) intents $ do
    replyIO <- bindSemToIO (\(c, t) -> void $ reply c t)
    P.embed $ forkIO $ startMyLoopWithCallback replyIO

    -- ...
Miezhiko commented 1 year ago

now I understand better. but I don't have Message, only ID (Snowflake Message), is it possible to get Message by having only numbers of message channel user...?

guess I need to use Calamity.Cache.Eff , or is there other possible ways? I also have Snowflake Channel and Guild if it may help

for now solved with

replyWithSnowflake ∷ (BotC r, HasID Channel Message) 
                  => (Snowflake Message, Text) 
                  -> P.Sem r ()
replyWithSnowflake (msgId, txt) = do
  maybeMsgFromId <- getMessage msgId
  case maybeMsgFromId of
    Just msgFromId -> void $ reply @Text msgFromId txt
    Nothing        -> pure ()

and generally it works! thank you