Sunday, October 30, 2011

It's taken for granted by many people that Haskell and static types are incompatible with prototyping and quick-and-dirty hacks.

I wanted to put together some OpenGL code that had a script for how a bunch of graphics should be displayed. It was essentially an imperative specfification for my program. For quick and dirty hacks, GLUT is tolerable. But even when programming in C/C++, it's not supportive of programming in a straightforward imperative style because it uses inversion of control. Many graphics applications are written in a state machine style where the state machine gets to tick once each time there is an event callback. This really doesn't fit the imperative script style.

But it is possible to reinvert inversion of control in any language that supports continuations. And that includes languages like Python that support linear continuations in the form of generators. But I'm using Haskell here.

Continuations reify the remainder of a computation. Or in more down to earth language: they allow you to grab the stuff you're about to do as a function, put it on ice for a while, and then carry on doing it later. So imagine we had a block of imperative code and that we'd like, at each GLUT callback, to make some progress through this block. We can use continuations like this: each time we want to yield control back to the main loop we simply grab the remainder or our 'script' as a continuation and make it the callback to be executed next time GLUT is ready.

The slight wrinkle is that OpenGL/GLUT calls use IO. To combine IO and continuations we need the ContT monad transformer.

I'll do everything except the yield function first and get back to that at the end.

The first thing to note is that render doesn't actually do any rendering. At the end of the day we can't tell GLUT when to render, it only calls you. So instead render tells GLUT what to do next time it's in the mood for a bit of rendering:

> render f = liftIO $ displayCallback $= f

That leaves one thing to explain: yield. It needs to grab the remainder of the script and package it up in a form suitable for installation as an idle callback. But there's a catch: continuations are notorious for making your head explode. If you're throwing together a quick and dirty hack, that's the last thing you need. Here's where static types come to the rescue. As Conor McBride points out, we want to just do the easy thing and follow gravity downwards.

So first we try to guess the type of yield. We know we're working with the ContT IO monad. So its type is going to be ContT IO a for some a. There's no particular type of data we want to get out of yield, it's just a thing we want executed. So we can guess the type is ContT IO (), the type () being the generic filler type when we don't actually have any data.

Let's look at the definition of ContT:

newtype ContT r m a = Cont {
runContT :: (a -> m r) -> m r
}

The type r is the final return type from our continuation. We're not interested in a return value, we just want to *do* stuff. So we expect r to be () as well.

So yield must essentially be of type (() -> IO ()) -> IO ().

So we want to concoct something of this type using GLUT's idleCallback function. As yield must take a function as argument it must look something like:

yield = ContT $ \f -> ...

We know that f is of type () -> IO (). So there's only one thing we can do with it: apply it to (). That gives us something of type IO (). That's precisely the type of GLUT's idleCallback. So we put it all together:

> yield = ContT $ \f -> idleCallback $= Just (f () )

The code now works. I didn't have to spend even a moment thinking about the meaning of a continuation. Implementing yield was about as hard as putting together a jigsaw puzzle with three pieces. There's only so many ways you can put the pieces together.

And that's a simple example of why I often like to write quick-and-dirty code with a statically typed language.

(Oh, and I'm not trying to take part in a war. I like to prototype in many different languages, some of which are dynamically typed.)

PS Note also that the above code illustrates one way to avoid IORefs in GLUT code.

13 comments:

Hello! You end your post tantalisingly with "PS Note also that the above code illustrates one way to avoid IORefs in GLUT code." Can this technique be used to avoid IORefs not just in GLUT code but in general, and would you be willing to provide a small example?

Many would argue that the list [-1, ..., 1.0000000000000018] is a better approximation to [-1, ..., 1.0] than [-1, ..., 0.9980000000000018]. I recommend avoiding enumFromThenTo with floating point types in production code.

Yeah well they'd be *wrong*. It violates the basic invariant of what an interval means. Note that they were not insane enough to use that logic on integer intervals, because then [0,3..5] would be [0,3,6] but it isn't because that would be crazy.

*Main> [0,3..5][0,3]*Main> [0,3..5.0] -- CRAZY![0.0,3.0,6.0]

Here is a simple but painful example. We define piA to approximate pi by naive numeric integration of the area of a 1/4 pie slice of the unit circle.

Yes, it's a bit crap. At some point someone decided that Float and Double are instances of Enum and had to make up some semantics for a bunch of functions that don't really make sense for Float and Double. It's easy to see why they chose the implementation they did, but I agree it's not good. Some of the bad type class decisions in the standard prelude are getting addressed and I hope this one will be too.

This code avoids IORefs by exploiting a particular feature of the structure of GLUT applications. Avoiding them in general is a different matter entirely. There are too many different ways to structure Haskell code to give a one size fits all solution.