Links: Up
This sample shows how to do advanced multi-threaded GUI (Graphical User Interface) programming where worker threads compute the results of an algorithm, in this case the Game of Life. This is one of the most advanced samples, exploiting many novel features of F# not found in other languages.
The F# code in alg.fs is the core computation of the rules of the Game of Life on a grid. An array of cells is used for obvious efficiency and simplicity reasons. The external interface to this component is alg.fsi.
The F# code in worker.fs specifies the reactive computation machine that runs on the worker thread (but not the worker thread itself). The automata is specified as a set of recursive functions via traditional control constructs, and is paramaterized by callbacks that provide the interface to the GUI.
The GUI and thread control logic can be found in client.fs. The specification of the GUI uses F#'s reactive 'runtime-checked' recursion, hence the compiler warnings when it is compiled.
GUIs are 'recursively reactive machines', e.g. a Form may influence entries within the Menu, the Menu may influence the Form, a Bitmap component may need to reference the entire display object in which it is embedded etc. This recursion is unavoidable and must be dealt with one way or another in the code that specifies a Form. The most natural thing in ML is to reach for a "let rec" binding to help build the recursive specification.
The problem with this is that the "let rec" construct of traditional ML languages is insufficient to allow this wiring to be expressed. In particular, the typical ML "let rec" construct allows at most three types of values to be declared, if that: function values; "lazy" values (which are delayed computations, and are implemented under-the-hood by function values) and immediately-recursive data (e.g. infinite lists like "let rec x = 1 :: x"). Objects such as Forms may not be part of the recursive set of values, and nor may the side-affecting actions that configure these objects (i.e. the actions that set the properties on a Form, where these actions are inherently part of the initialization of the object). There are of course good reasons for this language restriction: it lets these languages guarantee that a set of recursive values will never contain unrealizable invalid cycles. That means no errors can possibly occur when initializaing a set of mutually-referential values.
However, if truly recursive objects are present that also require non-trivial initialization (e.g. by calling external libraries), then the hacks needed to get around the "let rec" restriction can be ghastly. One common approach is to use reference cells, e.g. a cell storing the "display" which initially contains a "None" value and is updated at the point where the display object is first referenced. Although explicit, it is highly confusing and can still lead to horrible errors if the value is referred to before it is initialized, for example if it is inadvertently dereferenced when building an event closure. The verbosity of this approach does little to control the actual complexity.
Another common approach in functional languages is to rewrite the GUI API to include a sufficiently rich 'emedded' language of recursive components, e.g. a set of ML dataypes that cover a wide range of recursive wirings. However, this is an extremely difficult design exercise in itself, and no one has yet successfully written a highly usable ML widget library. Furthermore there are many other reasons not to attempt to rewrite such APIs when programming in a heterogeneous language system such as .NET, becuase, for example, your components may become unusable from other languages.
The approach taken by F# is to permit recursive bindings, but compile them such that each object and action involved in specifying the recursive system becomes a delayed ("lazy") value. These values are always referred to by dereferencing the lazy value, i.e. by resolving the delay. The actions involved in detting properties on the objects are also delayed actions. The final step is to resolve all the delays in one magic step, by forcing the evaluation of each delayed computation. This is similar (but not identical) to what would happen in Haskell, where the specification of such recursive systems is considerably easier, at the cost of I/O and other stateful operations being in general more difficult.