
Felipe Lessa wrote: (abridged)
Ryan Ingram enlightened us with MonadPrompt as a very nice abstraction for turn-based games, allowing easy programming and testing.
I wonder how nicely it fits on a Gtk2Hs application. =)
The problem lies in the fact that Gtk is event-driven, so every time we ask the user for something, we have to wait for the corresponding event that will bring us his answer.
The standard way of solving the problem of running two sequential things at once is using threads [...]This not only feels hackish, but also doesn't scale very well. [...]This feels hackish and fragile as well, specially because it is difficult to hunt down bugs.
But Prompt has a very *very* nice property we're missing to take advantage of here: it's is *pure*.
"If you wanted to add undo, all you have to do is save off the current Prompt in the middle of runPromptM; you can return to the old state at any time."
Eventually this feature rang some bells: you can save not only when you want to undo, but also when you want to ask something to the user.
let's see how:
lastAttempt :: AttemptCode lastAttempt showInfo entry button = do game <- guessGameNew runPromptM' game where -- Signature required runPromptM' :: Prompt GuessP Int -> IO () -- note: not (IO Int)! runPromptM' (Prompt (Print s) c) = showInfo s >> runPromptM' (c ()) runPromptM' (Prompt Guess c) = do mfix $ \cid -> do let cont guess = do {signalDisconnect cid; runPromptM' (c $ read guess)} onClicked button $ entryGetText entry >>= cont return () runPromptM' (PromptDone attempts) = do showInfo $ "You took " ++ show attempts ++ " attempts."
No threads, no MVars, no nothing. No freezes, no races, no exceptions, no overheads.
Marvelous work! Here's a short summary: The general idea was to implement a game in a modular way, namely such that the game and interaction logic is written in a separate monad which can be "plugged" into different interfaces like command line, undo, GUI etc. . MonadPrompt (aka the free monad) lets you do exactly that. Command line is straightforward, Ryan showed how to do undo and Felipe's post shows how to do it in a GUI framework which are notorious for being event-based and thus a tricky target for this particular task. The insight is that the actions from the MonadPrompt like Prompt Guess c or Prompt (Print s) c *are* the game state. What to do in the state Prompt Guess c -- some function c ? Well, we wait for the user to press a button, and then we feed the text he entered in some text field to c which advances the game to a new state. More precisely, we register a button event for that and return control to the GUI framework for the waiting. What to do with a monadic value Prompt (Print s) c ? Well, we print the message and immediately proceed with c , i.e. without waiting for user input this time. In the end, the only unusual thing about our game state is that it already know how to continue, namely via the continuation c . Felipe explored another option, namely to run the game monad in a thread that communicates with a separate GUI thread. Conceptually, this is closer to the feeling of the game monad, namely that it "runs through". But getting the communication between threads right is a nightmare. And in a sense, /threads are exactly the abstraction we wanted to implement in the first place/, only with more problems. How are threads themselves implemented? Well, in essence, the scheduler which runs threads just sees states like Prompt ReadMVar c -- this thread waits for a message Prompt (PutMVar x) c -- dispatch a message and proceed and evolves them. Of course, ghc's threads are closer to the machine and hence more fine-grained than that (pure computations can be suspended) with corresponding advantages (more interleaving, good for staying responsive while doing work) and drawbacks (loss of atomicity). The reader may want to try to implement his own (toy-) thread library with Prompt . Solution here: K. Claessen. Poor man's concurrency monad. http://www.cs.chalmers.se/~koen/pubs/jfp99-monad.ps For less "toy", see also P. Li, S. Zdancewic. Combining events and threads for scalable network services. http://www.seas.upenn.edu/~lipeng/homepage/papers/lz07pldi.pdf Maybe the property that distinguishes the harmless game monad from the dreaded threads is: they have forks. :) I mean, threads run in parallel and can/need to communicate and spawn whereas the game monad runs as one and is unable to soliloquize.
Eventually this feature rang some bells: you can save not only when you want to undo, but also when you want to ask something to the user. Unfortunately, I still haven't come up with a nice higher order function that generalizes this work without reinventing Prompt on an isomorphic type.
Oh, what kind of generalization do you have in mind? Regards, apfelmus