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.

ALERT! 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.

Topic attachments
I Attachment Action Size Date Who Comment
zipzip afp-cdshop.zip manage 24.4 K 26 Feb 2007 - 09:02 BastiaanHeeren Start version