Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No instance for (MonadMask (Sem DefaultMembers)) #395

Closed
teto opened this issue Dec 13, 2020 · 17 comments
Closed

No instance for (MonadMask (Sem DefaultMembers)) #395

teto opened this issue Dec 13, 2020 · 17 comments

Comments

@teto
Copy link

teto commented Dec 13, 2020

Hi,

I've spent the WE converting my MTL-based prototype to polysemy and I love it. Makes much more sense and has been a pleasure to work with (minus the polysemy plugin panics).

I am trying to use haskeline to write some REPL (code is not public but I could eventually make it public) and so I call my effects
runCache . logToIO from main with:

  _res <- runM . P.runState myState . runCache . logToIO $ do
       runInputT haskelineSettings inputLoop

-- type DefaultMembers = [Log, Cache, P.State MyState, Embed IO]
inputLoop :: InputT (Sem DefaultMembers) ()
...

This generates the error

No instance for (MonadMask (Sem DefaultMembers)) arising from a use of ‘runInputT’ • In a stmt of a 'do' block: runInputT haskelineSettings inputLoop In the second argument of ‘($)’, namely ‘do runInputT haskelineSettings inputLoop’ In a stmt of a 'do' block: _res <- runM . runState myState . runCache . logToIO $ do runInputT haskelineSettings inputLoop

and now I wonder how to work around this issue. I've seen polysemy-research/polysemy-zoo#61 but it's a bit too meta for me.

@tek
Copy link
Member

tek commented Dec 14, 2020

not sure this is gonna work, but you might be better off using Final (InputT IO) and lifting your InputT actions with embedFinal. obviously not a very flexible solution, but I assume the MonadMask issue is a hard limit

@teto
Copy link
Author

teto commented Dec 14, 2020

@tek thanks a lot for the help, I managed to improve things a bit but I am still stuck.
I've modified my inputLoop to be inputLoop :: (Sem [Final (InputT IO), Log, Cache, P.State MyState, Embed IO ]) ()
At some point in inputLoop, I call a function that returns (Sem DefaultMembers) () and I dont know to add the Final (InputT IO) to it.
I pushed my code at https://github.com/teto/quantum and my attempt at your recommandation at https://github.com/teto/quantum/tree/final (1 commit). I know it's a lot to ask but I would be very grateful if you or anyone could have a look.

@tek
Copy link
Member

tek commented Dec 14, 2020

@teto it's not a lot to ask at all, I'm happy to take a look!

Unfortunately, I can't build your project since it appears to depend on others in your file system. Furthermore, your shell.nix uses pkgs.llvm_11, which seems not to be present in your nixpkgs.

However, I can give you sufficient advice from looking at the code!
One of the problems you're facing is that you're working with a fixed effect stack in runCommand (DefaultMembers), which is especially constraining since you're using it for a higher-order callback (CommandCb).

The canonical way to specify the stack is to use Members DefaultMembers r, which you probably discarded because you assumed you'd have to specify the stack for the callback. Now if you really want to have your commands in a Map, I'm pretty sure changing CommandCb to be Members DefaultMembers r => Sem r RetCode should work the same way; I would strongly recommend, though, that you make it an effect Command, whose constructors correspond to the functions, like LoadPCap, and then dispatch those in an interpreter. Or, at least, just match on the command names and call the functions directly, using only Members instead of a fixed Sem type arg.

Please don't hesitate to ask for further advice!

@tek
Copy link
Member

tek commented Dec 14, 2020

oh, and please join us at https://funprog.zulipchat.com , we have a Polysemy stream there!

@tek
Copy link
Member

tek commented Dec 15, 2020

just realized I forgot the most significant bit about the Map!

You can simply change the signature of commands to Members DefaultMembers r => Map Text (Sem r ())!
That basically means that the function requests the stack to be whatever has been determined for inputLoop, as long as all effects in DefaultMembers are somewhere in there. Then it creates the Map with that r, passing it along to all the callbacks in there.
This also means that the callbacks do not all have to commit to that same stack, but can just request the effects they need with their own Members constraints, even if they aren't identical! The r will be resolved to contain all dependencies, and then the return types of all callbacks will be filled with the same stack.

@teto
Copy link
Author

teto commented Dec 16, 2020

thanks for the help. So you suggest to remove runCommand and CommandCb ?
I kinda like the idea but I seem to hit a wall with my commands map:
Members DefaultMembers r => Map Text (Sem r ()) is a nice trick (I think in my case I wanted a Members DefaultMembers r => Map Text (Sem r CMD.RetCode) but it still seems to pose problems later on. Ideally I would like to keep it as I would like to make a haskell copy of https://github.com/python-cmd2/cmd2 (REPL with autocompletion, haskeline on steroids if you want) but eventually I will settle on anything that compile/works. Same reasoning for making an effect Command : i would like to try without first.

I've tried some things but maybe I am too tired (or stupid), I can't make anything work. I've pushed several modifications so that compilation should at least start without any fiddling at https://github.com/teto/quantum/tree/final. I hope to give it a try tomorrow with a fresher mind.

@tek
Copy link
Member

tek commented Dec 16, 2020

well, clear your head and then post some compile errors, we'll figure it out 🙂

as for the Map, nothing wrong with using that! I would suggest, though, to first parse the strings into an ADT, maybe with optparse-applicative; then you can match on it separately for completion and dispatch!

@tek
Copy link
Member

tek commented Dec 16, 2020

some preliminary pointers:

  • inputLoop should also use Members (you can use a fixed stack, but Final should be at the end!!)
  • Embed IO needs to be lifted to InputT IO as well, with embedToFinal and either embedToEmbed or finalToFinal, or you use Embed (InputT IO) and lift
  • runCommand, in its current trivial state, does not need the Members

@teto
Copy link
Author

teto commented Dec 16, 2020

I am stuck on the 2 very problem/pointers you mentioned : embed IO and runFinal.
at some point I need to call runInputT but not sure where.
_res <- runFinal . runCache . logToIO . P.runState myState $ inputLoop

_res <- runFinal . runCache . logToIO . P.runState myState  $ inputLoop | • Unhandled effect 'Embed IO' Probable fix: add an interpretation for 'Embed IO' • In the first argument of ‘(.)’, namely ‘logToIO’ In the second argument of ‘(.)’, namely ‘logToIO . runState myState’ In the second argument of ‘(.)’, namely ‘runCache . logToIO . runState myState’

and

_res <- runFinal . runCache . logToIO . P.runState myState  $ inputLoop | • Unhandled effect 'Final (InputT IO)' Probable fix: add an interpretation for 'Final (InputT IO)' • In the second argument of ‘($)’, namely ‘inputLoop’ In a stmt of a 'do' block: _res <- runFinal . runCache . logToIO . runState myState $ inputLoop In the expression: do cacheFolderXdg <- getXdgDirectory XdgCache "mptcpanalyzer2" doesDirectoryExist cacheFolderXdg >>= \case True -> putStrLn ("cache folder already exists" ++ show cacheFolderXdg) False -> createDirectory cacheFolderXdg let myState = MyState {_cacheFolder = cacheFolderXdg, _loadedFile = Nothing, _prompt = promptSuffix} options <- execParser opts putStrLn "Commands" print $ extraCommands options _res <- runFinal . runCache . logToIO . runState myState $ inputLoop putStrLn "Thanks for flying with mptcpanalyzer"

I logged in to the zulip but couldn't find any polysemy stream (even via the search engine) ?

NB: I've noticed sometime the order in evaluating effect matters in terms of compilation (I know it matters at runtime ?), was it an artifact of refactoring or a fixed stack vs freeform or an hallucination ?

@tek
Copy link
Member

tek commented Dec 16, 2020

curious, are you sure you're on the right server? this is the direct link to the stream: https://funprog.zulipchat.com/#narrow/stream/216942-Polysemy

I would recommend trying to build smaller working programs first, then extending those with your app code.
A good starting point would be

runInputT $ runFinal @(InputT IO) $ embedFinal $ pure ()

Then try embedding an IO, like embedFinal @(InputT IO) $ lift $ putStrLn "works".

In order to convert an Embed IO to Final (InputT IO), you'll have to call runFinal . embedToFinal . embedToEmbed lift runInputT or similar.

And lots of type annotations are very helpful!

@teto
Copy link
Author

teto commented Dec 17, 2020

thanks that was super helpful.
So this compiles (I added my cache effect)

main = ...
  _ <- runInputT haskelineSettings 
        $ runFinal @(InputT IO)
        $ runCache testLoop

testLoop :: Members '[ Cache, Log, Final (InputT IO)  ] r => Sem r ()
testLoop = do
  return ()

so then I try to add my log effect

main = ...
  _ <- runInputT haskelineSettings 
        $ runFinal @(InputT IO)
        $ logToIO testLoop
        $ runCache testLoop

testLoop :: Members '[ Cache, Log, Final (InputT IO)  ] r => Sem r ()
testLoop = do
  -- _ <- getInputLine
  -- logInfo "test"
  return ()

but the logToIO interpreter adds a constraint (Embed IO) not visible in testLoop and so the compiler complains about Embed IO not being interpreted ("No instance for (Find (Embed IO) '[]) arising from a use of ‘logToIO"/"Unhandled effect 'Embed IO'") .
I first though I could interpret (Embed IO) and Final (InputT IO) separately but according to your advice, I need to convert the constraint Embed IO added by logToIO to Final (InputT IO).
In order to convert an Embed IO to Final (InputT IO), you'll have to call runFinal . embedToFinal . embedToEmbed lift runInputT or similar.
I could not find the embedToEmbed function and I am a bit surprised to see the runInputT on the right ? in the end I cound't manage to convert Embed IO into Final (InputT IO) yet.

        $ runFinal @(InputT IO) $ P.embedToFinal   -- the Embed IO -> @(Input IO) is still unclear
        $ logToIO
        $ runCache testLoop

@tek
Copy link
Member

tek commented Dec 17, 2020

oh right, I thought the equivalent of finalToFinal was named similarly, but it's actually runEmbedded!
finalToFinal needs bidirectional conversion of the involved monads, hence the runInputT, but you don't need it with runEmbedded. So embedToFinal . runEmbedded lift should do it!

Wouldn't be surprised if you'd need to add type applications there.

@teto
Copy link
Author

teto commented Dec 17, 2020

I am happy to announce the end of my quest:

  _ <- runInputT haskelineSettings $
          runFinal @(InputT IO) 
          $ P.embedToFinal . P.runEmbedded lift
          $ P.runState myState
          $ runCache
          $ logToIO inputLoop

works.
much love to @tek .

Now it seems that in my architecture

commands :: Members DefaultMembers r => HM.Map String ([String] -> Sem r CMD.RetCode)
commands = HM.fromList [
    -- ("load", loadPcap)
    ("load_csv", loadCsv)
    -- , ("list_tcp", listTcpConnections)
    , ("help", printHelp)
    -- , ("list_mptcp", listMpTcpConnections)
    ]

DefaultMembers must be the union of the effects of the different entries, if I forget one effect in default member, the ocmpiler complains. I had thought that maybe it could deduce the correct hashmap value type by itself ?
I wonder if it's possible otherwise maybe I'll just have to implement the command as an Effect.

@tek
Copy link
Member

tek commented Dec 17, 2020

awesome!!

If you wanted each entry to have a separate list of Members (if I understand you correctly), you would have to use a dependent map. It might be possible, but you won't have any benefit from it if you use the map only in one place (inputLoop).

@teto
Copy link
Author

teto commented Dec 18, 2020

yes you understood me correctly. Didn't know about dependant map but this looks overkill. As you say the commands shall be called only from inputLoop so implementing Command as an effect should be doable.
I dont know if that's the best approach so if there is a project with a similar flow, I would be curious to have a look at.
As a reminder for myself https://packdeps.haskellers.com/reverse/polysemy.

Once again thanks a lot @tek !

@teto teto closed this as completed Dec 18, 2020
@tek
Copy link
Member

tek commented Dec 18, 2020

I'd still highly recommend you try finding us on Zulip!

@teto
Copy link
Author

teto commented Dec 18, 2020

haha I am on the zulip already (stalking). This week was too busy to interact but soon for sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants