[Haskell-cafe] Re: MonadPrompt + Gtk2Hs = ?

apfelmus apfelmus at quantentunnel.de
Sun Jan 13 15:49:49 EST 2008

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.

For less "toy", see also

   P. Li, S. Zdancewic. Combining events and threads for scalable
   network services.

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?


More information about the Haskell-Cafe mailing list