m (links to TypeCompose and Applicative Data-Driven Programming)
m (→Abstract: Link)
Revision as of 14:44, 2 June 2007
Phooey is a functional UI library for Haskell. Beside this page, here are some ways to explore Phooey:
- Read the Haddock docs (with source code, additional examples, and Comment/Talk links).
- Get the code repository: darcs get http://darcs.haskell.org/packages/phooey, or
- Grab a distribution tarball.
- See the version changes.
Phooey is also used in GuiTV, a library for composable interfaces and "tangible values".
For a draft paper about the implementation of (a future version of) Phooey, please see the paper Applicative data-driven programming.
The implementation of data-driven computation in Phooey is provided by the TypeCompose library.
GUIs are usually programmed in an unnatural style, in that implementation dependencies are inverted, relative to logical dependencies. This reversal results directly from the push (data-driven) orientation of most GUI libraries. While outputs depend on inputs from a user and semantic point of view, the push style imposes an implementation dependence of inputs on outputs.
A second drawback of the push style is that it is imperative rather than declarative. A GUI program describes actions to update a model and and view in reaction to user input. In contrast to the how-to-update style of an imperative program, a functional GUI program would express what-it-is of a model in terms of the inputs and of the view in terms of the model.
The questions of push-vs-pull and imperative-vs-declarative are related. While an imperative GUI program could certainly be written to pull (poll) values from input to model and model to view, thus eliminating the dependency inversion, I don't know how a declarative program could be written in the inverted-dependency style. (Do you?).
A important reason for using push rather than pull in a GUI implementation is that push is typically much more efficient. A simple pull implementation would either waste time recomputing an unchanging model and view (pegging your CPU for no benefit), or deal with the complexity of avoiding that recomputation. The push style computes only when inputs change. (Continuous change, i.e. animation, negates this advantage of push.)
Phooey ("Phunctional ooser ynterfaces") adopts the declarative style, in which outputs are expressed in terms of inputs. Under the hood, however, the implementation is push-based (data-driven). Phooey performs the dependency inversion invisibly, so that programmers may express GUIs simply and declaratively while still getting an efficient implementation. I have taken care to structure Phooey's implementation as simply as possible to make clear how this dependency inversion works (subject of paper in progress). In addition, Phooey supports dynamic input bounds, flexible layout, and mutually-referential widgets.
Phooey came out of Pajama and Eros. Pajama is a re-implementation of the Pan language and compiler for function synthesis of interactive, continuous, infinite images. Pan and Pajama use a monadic style for specifying GUIs and are able to do so because they use the implementation trick of Compiling Embedded Languages, in which one manipulates expressions rather than values. (This trick is mostly transparent, but the illusion shows through in places.)
3 One example, three interfacesAs an example, below is a simple shopping list GUI. The
Phooey presents three styles of functional GUI interfaces, structured as a monad, an arrow, and an applicative functor. Below we present code for the shopping list example in each of the three functional styles.The examples below are all found under
src/Examples/in the phooey distribution, in the modules
Applicative.hs. In each case, the example is run by loading the corresponding example module into ghci and typing "
ui1 :: UI (Source ()) ui1 = title "Shopping List" $ do a <- title "apples" $ islider (0,10) 3 b <- title "bananas" $ islider (0,10) 7 title "total" $ showDisplay (liftA2 (+) a b)
The relevant library declarations:
-- Input widget type (with initial value) type IWidget a = a -> UI (Source a) -- Output widget type type OWidget a = Source a -> UI (Source ()) islider :: (Int,Int) -> IWidget Int showDisplay :: Show a => OWidget a title :: String -> UI a -> UI a
Before we move on to other interface styles, let's look at some refactorings. First pull out the slider minus initial value:
sl0 :: IWidget Int sl0 = islider (0,10)
Then the titled widgets:
apples, bananas :: UI (Source Int) apples = title "apples" $ sl0 3 bananas = title "bananas" $ sl0 7 total :: Num a => OWidget a total = title "total" . showDisplay
And use them:
ui1x :: UI (Source ()) ui1x = title "Shopping List" $ do a <- apples b <- bananas total (liftA2 (+) a b)
fruit :: UI (Source Int) fruit = liftM2 (liftA2 (+)) apples bananas ui1y :: UI (Source ()) ui1y = title "Shopping List" $ fruit >>= total
Using source types allows the monadic style to capture the static nature of the input GUI while giving access to a source of dynamic values. Alternatively, we can solve the problem by replacing the Monad abstraction with one that separates static and dynamic aspects. Getting that separation is the point of the Arrow abstraction, and thus Phooey provides an arrow interface as well. Moreover, the UI arrow is implemented on top of its UI monad using a simple, reusable pattern. See the Arrow interface doc and its source code.
ui1 :: UI () () ui1 = title "Shopping List" $ proc () -> do a <- title "apples" $ islider (0,10) 3 -< () b <- title "bananas" $ islider (0,10) 7 -< () title "total" showDisplay -< a+b
type IWidget a = a -> UI () a type OWidget a = UI a ()
3.3 Applicative Functor
Applicative functors provide still another approach to separating static and dynamic information. Here is our example, showing just the changes relative to the monadic version. (See the Applicative interface doc and its source code.)
ui1 :: UI (IO ()) ui1 = title "Shopping List" $ fruit <**> total fruit :: UI Int fruit = liftA2 (+) apples bananas total :: Num a => OWidget a total = title "total" showDisplay
The UI-building functions again have the same types as before, relative to these new definitions:
type IWidget a = a -> UI a type OWidget a = UI (a -> IO ())
- Output widgets are function-valued UI.
- has a simpler definition, requiring only one lifting instead of two.fruit
- is subtly different, because output widgets are now function-valued.total
- uses the reverse application operatorui1. This reversal causes the function to appear after (below) the argument.(<**>)
- is an IO-valued UI.ui1
4 Dynamic bounds
Phooey sliders may have dynamic bounds, taking a source of bounds instead of static bounds. In the following example, the first two sliders determine the bounds of the third slider.
Of course, one would want a prettier interface, but this example will serve to illustrate a point.
4.1 Dynamic bounds, monad version
In the Monad version, the new function is
isliderDyn :: Source (Int,Int) -> IWidget Int
ui2 :: UI (Source ()) ui2 = do l <- title "lo" $ sl0 3 h <- title "hi" $ sl0 8 v <- title "val" $ isliderDyn (liftA2 (,) l h) 5 title "factorial" $ showDisplay (liftA fact v)
lo,hi :: UI (Source Int) lo = title "lo" $ sl0 3 hi = title "hi" $ sl0 8 pair :: Applicative f => f a -> f b -> f (a,b) pair = liftA2 (,) bounds :: UI (Source (Int,Int)) bounds = liftM2 pair lo hi val :: UI (Source Int) val = do b <- bounds title "val" $ isliderDyn b 5 ui2 = do v <- val title "factorial" $ showDisplay (liftA fact v)
As a variation, we might prefer to wrap the "val" title is around the lo & hi sliders as well the val slider. This layout reflects the purpose of the "lo" and "hi" sliders.
The only change:
val = title "val" $ do b <- bounds isliderDyn b 5
4.2 Dynamic bounds, arrow version
ui2 = proc () -> do l <- lo -< () h <- hi -< () v <- title "val" $ isliderDyn 5 -< (l,h) title "factorial" showDisplay -< fact v
ui2 = proc () -> do lo <- title "lo" $ isliderDyn 3 -< (0,10) hi <- title "hi" $ isliderDyn 8 -< (0,10) val <- title "val" $ isliderDyn 5 -< (lo,hi) title "factorial" showDisplay -< fact val
We can also do some factoring. The bounds come out very simply:
bounds :: UI () (Int,Int) bounds = lo &&& hi
val = bounds >>> title "val" (isliderDyn 5) ui2 = (fact ^<< val') >>> title "factorial" showDisplay
ui2 = val >>> pure fact >>> title "factorial" showDisplay
val = title "val" $ (lo &&& hi) >>> isliderDyn 5
4.3 Dynamic bounds, applicative functor version
The example code is very simple:
val = title "val" $ isliderDyn (pair lo hi) 5 ui2 = (fact <$> val) <**> title "factorial" showDisplay
This version includes the bounds within the "val" title. I don't know how to get a "val" title on just the dynamically-bounded slider.
5 LayoutBy default, UI layout follows the order of the specification, with earlier-specified components above later-specified ones. This layout may be overridden by explicit layout functions. For instance, the following definitions form variations of
GUIs & code:
uiB1 = fromBottom ui1 uiL1 = fromLeft ui1
We can also lay out a sub-assembly, as in
ui3 = fromBottom $ title "Shopping List" $ fromRight fruit >>= total
6 Recursive GUIsNext is a recursive example. It is like
uir1 :: UI (Source ()) uir1 = mdo l <- title "lo" $ isliderDyn (pair (pure 0) h) 3 h <- title "hi" $ isliderDyn (pair l (pure 10)) 8 v <- title "val" $ isliderDyn (pair l h) 5 title "factorial" $ showDisplay (liftA fact v)
boundsR :: UI (Source (Int,Int)) boundsR = mfix boundsF where boundsF lh = liftM2 pair (title "lo" $ isliderDyn (pair (pure 0) h) 3) (title "hi" $ isliderDyn (pair l (pure 10)) 8) where (l,h) = unPair lh unPair :: Functor f => f (a, b) -> (f a, f b) unPair p = (fmap fst p, fmap snd p)
valR :: UI (Source Int) valR = do b <- boundsR title "val" $ isliderDyn b 5 uir1' = do v <- valR title "factorial" $ showDisplay (liftA fact v)
The next example is tightly recursive. A slider is used to bound itself, so that the range is always the current value ±5.
uir2 = mdo v <- title "val" (isliderDyn (liftA (plusMinus 5) v) 6) title "squared" (showDisplay (liftA square v)) where plusMinus n x = (x-n,x+n) square y = y*y
The arrow and applicative functor versions of these examples exhaust stack space.
wxHaskell is therefore built on top of wxWidgets -- a comprehensive C++ library that is portable across all major GUI platforms; including GTK, Windows, X11, and MacOS X.
So I expect that Phooey runs on all of these platforms. That said, I have only tried Phooey on Windows. Please give it a try and leave a message on the talk page.
8 Known problems
- Recursive examples don't work (consumes memory) in the Arrow or Applicative interface.