Today's posting is by a guest author, Jon Dowland.
I wanted to refresh my Haskell skills recently so I picked up an old idea I'd had of writing a successor to WadC in Haskell. WadC is a LOGO-like language for making 2D drawings (Doom maps). You write a sequence of instructions that move a "pen" around and draw lines, place things, etc. It looks a bit like this:
...
movestep(32,32)
thing
straight(256)
rotright
straight(512)
rotright
...
The driving force behind for writing a successor to WadC is to work around one of its most serious limitations: you do not have direct access to the data structure that you are building. Once you place a line, it's defined and you can't change it, or even query existing drawn lines to find out if it exists. The appeal of using Haskell would be to give you access to the data structure that's "behind the scenes" as well as Haskell's rich features and libraries.
WadC's syntax, like LOGO, resembles an ordered sequence of operations that manipulate some hidden state relating to the Pen: its orientation, whether its on the "paper" or not, what colour line is being drawn, etc. I thought this would look particularly nice in Haskell's "do notation" syntax, e.g.:
blah = do
down
straight 64
rotright
straight 128
rotright
...
Ignoring the syntax issue for a minute and thinking just about types, the most
natural type to me for the atomic operations would be something like Context ->
Context
: simply put, each function would receive the current state of the world,
and return a new state. For example Context
could be something like
data Context = Context { location :: Point -- (x,y)
, orientation :: Orientation -- N/E/S/W
, linedefs :: [Line] -- Line = from/to (x,y)/(x,y)
, penstate :: PenState -- up or down
...
An example of a simple low-level function that would work with this:
down c = c { penstate = Down }
The immediate advantage here over WadC is that the functions have access to all of the existing context: they could not only append lines, say, but replace any existing lines too. I was envisaging that you might, for example, adjust all drawn lines as if you were using a completely different geometric model to get some weird effects (such as one axis being warped towards a single point). Or perhaps you would super-impose two separate drawings and have fine control over how overlapping regions have their properties combined (one overrides the other, or blend them, etc.)
The problem is in uniting the simple types with using do-notation, which
requires the routines to be "wrapped up" in Monads. I got a prototype working
by writing a custom Monad, but it required me to either modify the type of each
of the low-level functions, or wrap each invocation of them inside the
do-block. If you imagine the wrap function is called wadL
, that means either
down = wadL down' where
down' c = c { penstate = Down }
or
blah = do
wadL $ down
wadL $ straight 64
wadL $ rotright
Both approaches are pretty ugly. The former gets ugly fast when the functions concerned are more complicated in the first place; the latter pretty much throws away the niceness of using do-notation at all.
An alternative solution is one that the
Diagrams package uses: define a new
infix operator which is just function composition (.
) backwards:
down &
straight 64 &
rotright
(Diagrams uses #
but I prefer &
)
By playing around with this I've achieved my original goal of refreshing my Haskell skills. Unfortunately I don't have the time or inclination to continue with this side-project right now so I haven't published a working Haskell-powered Doom map editor.
If I return to this again, I might explore writing a completely distinct language (like WadC is) with the compiler/interpreter likely written using Haskell.