Exercise WXHaskell
Afp0607
XTC: Extended and Typed Controls for wxHaskell
In the second exercise for AFP, we will extend the collection of controls offered by wxHaskell. We will
implement two
typed controls.
These controls are built on top of existing controls: in fact, we will model these
controls as subclasses of the original controls (using phantom type encoding).
For this, we need some really low-level, untyped hooks that are provided by
WXCore.
Controls in wxHaskell typically support the
text attribute (of type
String) which allows you to
get and set the current value of the control. The value that must be entered, however, does usually not
correspond to a String (but, for instance, a value of type
Int).
As a result, the programmer has to convert the value from and to a
String all the time. For example,
in order to interpret
the
String, the value has to be parsed first. Our typed controls will solve this problem
once and for all: they carry a typed value instead of a
String! Controls that allow you to make a
selection (e.g., a number of radio buttons) have an attribute
selection of type
Int. For these controls,
the same problem arises. In addition to typed controls, we will have a look at the observer-observable pattern
from OO languages in a functional setting. This pattern allows us to organize the
call-back functions of
our controls in a (more) elegant way.
To start, download
afp-cdshop.zip. This file should contain:
-
Observable.hs (implements the Observable pattern)
-
CDShop.hs (an example GUI)
- Seven
.gif files
The module
Observable offers a type class
Observable, some helper-functions that operate on an observable, and one
implementation of an observable called a
Control (an instance of the type class). This instance is only given to
illustrate the concept: we will not use it for implementing our typed controls. The module
CDShop is an example
graphical user interface (GUI) that we use for testing our typed controls.

To see the documentation (generated by Haddock) for wxHaskell, click
here.
Observables
Before we start, we first will have a look at the module
Observable, which defines the following type class:
class Observable obs val | obs -> val where
getValue :: obs -> IO val
setValue :: obs -> val -> IO ()
getObservers :: obs -> IO [val -> IO ()]
addObserver :: obs -> (val -> IO ()) -> IO ()
Observable is a multi-parameter type class: the first type argument represents the object that is observable, the
second type argument corresponds to the
value the observable is carrying. The functional dependency that is part
of the class declaration (
obs -> val in the first line) states that the type of an observable uniquely determines
the type of the value it is carrying.
The module also defines the data type
Control, which is an instance of the type class mentioned above. A control
consists of a model (a pointer to the current value) and a pointer to a list of call-back functions. These call-back
functions are called if the value changes. Study the instance declaration closely, in particular the two type
arguments of the type class
Observable. Also have a look at the module header, which declares explicitly the
functions, types, and classes that are exported. Constructor
C, for instance, is not visible outside this module.
Question: Are the two
IORefs really necessary in the
Control data type? What if we would change the declaration
into:
data Control a = C
{ model :: a
, observers :: [a -> IO ()]
}
Would this datatype be suitable for modeling an observable? Explain your answer.
Typed text entries
We are now ready to implement our own typed controls! Lets make a module
XTC.hs that contains the following
two type classes:
class TypedValue w t | w -> t where
typedValue :: Attr w t
class Labeled a where
toLabel :: a -> String
The first type class is the counterpart of the
text attribute (of type
String) offered by the wxHaskell library,
except that the type
t of this attribute depends on the widget
w. The second type class contains all types that
can be labeled (or shown). Also include the following helper-function for parsing a value (using the
Read type class).
parseRead :: Read a => String -> Maybe a
parseRead s = case reads s of
[(a, "")] -> Just a
_ -> Nothing
Step 1: Our first typed control will be a
subclass of the
TextCtrl control, which can be found in the wxHaskell library.
Introduce a type (either by a
data,
newtype, or
type declaration)
TypedEntry t a, where
t is the type carried by the widget,
and
a is used to model inheritance. Hint: for this you will have to introduce a phantom type. See the lecture notes.
Step 2: Next, define two constructor functions for a typed text entry:
typedEntry :: (Read t, Show t) => Window ?? -> [Prop (TypedEntry t ??)]
-> IO (TypedEntry t ??)
typedEntryExpl :: Window ?? -> (String -> Maybe t) -> (t -> String) -> [Prop (TypedEntry t ??)]
-> IO (TypedEntry t ??)
The first constructor function uses the type classes
Read and
Show for parsing and showing a value, respectively. The
second constructor function must be passed a parser and a pretty-printer explicitly (second and third argument). Only the
last one has to be defined from scratch:
typedEntry can be defined in terms of
typedEntryExpl.
Some notes:
- The question marks in the type signatures should be filled in to model inheritance correctly.
- Under the hood, a typed entry is nothing but a normal entry. Use
objectCast :: Object a -> Object b to turn a normal entry into a typed entry. This primitive function is provided by the WXCore layer (hence, include import Graphics.UI.WXCore). Yes, this function indeed enables you to defeat the type system.
- To implement our typed control correctly, we have to work around the type system once more. For that, we use the following two primitive functions (again, provided by
WXCore).
objectSetClientData :: WxObject a -> IO () -> b -> IO ()
unsafeObjectGetClientData :: WxObject a -> IO (Maybe b)
With these two functions, we can store some additional information with a widget (in our case, a typed control)
and at a later point retrieve the stored information. This part is completely untyped: if you make an error, your
program will certainly crash (with the most terrifying message you have ever seen). Setting the client data
will be done at our constructor function. You will have to use
unsafeObjectGetClientData later to implement the
four member functions of the
Observable type class (see the next part).
The three arguments of
objectSetClientData are: the widget that stores the additional data, the action to be
performed if the widget is deleted (in our case, just
return ()), and the information to be stored.
Step 3: Of course we want to make
TypedEntry an instance of Observable. Because the text entered by a user does not
necessarily have to be valid (it cannot be interpreted as a value of type
t), we will declare that the
value associated with a
TypedEntry t a is of type
Maybe t (where
Nothing corresponds to no parse).
instance Observable (TypedEntry t a) (Maybe t) where
getValue = undefined
setValue = undefined
getObservers = undefined
addObserver = undefined
It is up to you to implement the four member functions.
Some additional hints:
- Use the function
unsafeObjectGetClientData to access the information stored at the constructor function.
- Access the attribute
text via the functions get and set to obtain the current value of the text entry (of type String).
After having implemented the
getValue and
setValue functions, it is easy to implement the
typedValue attribute.
Just include the following code:
instance TypedValue (TypedEntry t a) (Maybe t) where
typedValue = newAttr "typedValue" getValue setValue
Step 4: The final step is to install an
on leave event handler at your typed entry. The idea is as follows: if the text
of your typed text entry is valid, then the text will appear in blue. Otherwise, in case of
Nothing, it will
appear in red. Use the
color attribute for this.
Typed radio buttons
Do the same steps to create a typed radio box.
- Create a type
TypedRadioBox t a that extends a normal RadioBox (again, using the phantom type approach).
- Make two constructor functions: the first one uses the type class
Labeled to turn a value into a String, the second receives this presentation function explicitly.
- The constructor function does not receive a
[String] as the original constructor function radioBox does. Instead, it receives a [t] where t is the type carried by the typed radio box.
- Make
TypedRadioBox an instance of both Observable and TypedValue. Is the Maybe still necessary in the instance declaration for Observable?
Question: What is the advantage of introducing a new type class
Labeled instead of reusing the existing type class
Show?
Example: a cd shop
To test your typed controls, use them in the cd shop application. Because of the subtyping, only changing the constructors
of the text entries and radio buttons should suffice. Implement the following features (by installing event handlers, and
adding observers to your controls):
- By selecting your favourite genre using the radio buttons, some cd's become invisible. Note that the data type
Genre only has three constructors. Can you add a fourth option, namelely "All", without adding a constructor to Genre? Selecting the "All" option implies that all cd's are visible.
- Implement the decrement, increment, and reset buttons. Decrement and reset are enabled when the current value is larger than zero. If the current value is invalid, then the decrement and increment buttons should be disabled as well.
- Implement the reset all button, which resets all cd counters to zero.
- The number of purchased items and the total price of the selected cd's should appear at the bottom (and should be constantly updated).
In case parts of this exercise are underspecified, any reasonable solution will do.
If you get stuck on this exercise, contact me.
Good luck!
Acknowledgement: Martijn Schrage and Arjan van IJzendoorn were the first to report the absence of typed controls in the
wxHaskell library during the development of the Bayesian network toolbox Dazzle.