[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