[Haskell-cafe] advice on architecting a library (and haskell software in general)

Yang hehx0sk02 at sneakemail.com
Thu Jan 11 17:40:53 EST 2007


hi all, i'm looking for advice on how to architect a simple widget
library for vty/hscurses, but these beginner-level questions should
apply to haskell applications in general. input/requests on any aspect
of my design would be greatly appreciated - in return perhaps you'll
get something you'd actually want to use!

i have studied in detail various haskell curses/vty apps, including
yi, hmp3, riot, and hscurses.widgets/contactmanager. my immediate goal
is to produce a set of composable widgets and a flexible framework
that - as much as possible - reduces the amount of code a user has to
write. my eventual goal is to make a functional gui library, along the
lines of fruit/wxfruit/fg. to this end i've also read their
literature, but i still do not understand enough about
arrows/yampa/afrp.

i currently have a tree of widgets, some of which can receive focus.
this allows them to alter the program's key handling, but in a certain
order based on the hierarchy. e.g., in my program, at the top-level,
hitting either 'q' or 'f12' quits the program. when we focus on a
container, pressing 'tab' will cycle focus among the subwidgets. when
we focus on a text box, the key bindings layer, so that 'q' inserts a
character into the text box, and 'f12' still exits and 'tab' still
cycles focus.

here's a simplified synopsis of the types (omits details like layout):

class Widget w where output :: Bool {- whether we have focus -} -> w -> Output
data Output = Output Image (Maybe CursorPos) KeyHandler
type Image = [[(Char,Attr)]] -- Attr is just the text decoration (colors)
type CursorPos = (Int,Int)
type KeyHandler = ???

i'm guessing the most flexible type for KeyHandler could be Key -> IO
(), but is this really the only/best approach? (every time i fall back
to IO, i feel i'm missing something/a puppy dies/etc.)

currently, this is what i have:

type KeyHandler = Key -> AppState -> AppState -- maps to a state updater
data AppState = AppState { rootWidget :: Widget, actualAppState :: ..., ... }

but this has a number of apparent issues, including:

- no decoupling of UI and app - that is, the key handlers that the
widgets return have complete knowledge of the application state. so
for instance, this particular text box knows that its key handler
should be:

handler key state =
  let oldPos = cursorPos $ someTextBox $ rootWidget $ state
  in case key of
       Left -> state { rootWidget { someTextBox { cursorPos = oldPos - 1 } } }
       ...

- on key press, can't save to a file or otherwise do anything in IO.

- this isn't going to scale - as my AppState grows and grows, we're
throwing away and reconstructing a lot of state. and the coding style
it demands is pretty clumsy, as demonstrated above.

but if we switch to IO ():

- still doesn't help decouple the library from the app. the above
example key handling code snippet would be the same (i.e. still very
clumsy), except that we'd be reading/writing the state from/to an
IORef or MVar.

- certainly, not all actions need IO. in fact, my current application
is just a viewer, and thus needs no IO at all.

- requires a global IORef or MVar

- i don't know how to address the performance problem without
resorting to sprinkling IORefs or MVars everywhere in the state
structure, thus strangling the app into IO everywhere

other open questions:

- how should the "top-most" code work? currently, my app's main has a
tiny event loop that feeds keys through a Chan to a State-ful function
([Key] -> State AppState [Image]) and then to the final drawing
function ([Image] -> IO ()). however, depending on the resolutions to
the above issues, this may radically change.

- how should i compose the various key handlers? this, again, will depend.

related work:

yi/hmp3 also have one large piece of state in a global mvar (allows
multiple threads to update it/trigger a redraw, instead of only
redrawing in response to key events), with no attempt to decouple the
UI and app parts of that state. event handling is done by a lexer that
matches regex key patterns to IO () - this doesn't couple key handling
with the UI components, and is thus not composable. (i thought the
idea of using a lexer as the state automata was good - there may be
some way to make this more composable, too, if the regex ever fails.)

riot's UI code operates mostly in StateT Riot IO (), where Riot is
again a monolithic application state. again, no attempt at decoupling
or composability is made. hscurses.widgets operates mostly in IO (),
and 'activating' (focusing) on widgets hands over the event loop
completely.



i hope i explained my design problems clearly. i've used haskell a
bunch for various small text-processing scripts, but decided to try to
use it for a "real" application that has little to do with parsing or
other purported strengths of the language. i believe other new
haskellers may relate to some of these issues.

thanks!

yang


More information about the Haskell-Cafe mailing list