Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Working with DSLs and Computation Expressions in F#

Using domain-specific languages (DSLs) and F# computation expressions will save you from writing UI code. Find out how.


advertisement
f you develop UI applications in F#, you probably miss the familiar Visual Studio form editor that comes with C# or VB. This is due to a simple reason: there are no partial classes in F#. This is precisely what the code generator behind the scenes would need to synthesize code for your design forms. While the F# team is working hard to find the best approach to support partial classes and bring the Visual Studio integration up to par with that of mainstream front-ends, there are a number of things you can do to bypass this soon-to-disappear annoyance.

For one, you can set up a multi-project solution and implement the UI as a C# or VB UI project, and code the rest of the application logic in F#. This works so well that it is hard to argue that you could do better unless you had in turn the ability to design the UI with F# code behind. But after you start making incremental changes to your UI project, various small issues start to crop up. One such issue is the accessibility of certain UI elements that you need for wiring event handlers; these need to be manually made public in your UI library project.

In this article, you will see two different approaches for making your UI coding easier and still retain all your application code in F#. In the first approach, you will develop a DSL to describe UI elements and their layout, and then implement a tool that generates F# code from this specification. While this has a sizeable gain over writing ordinary UI code in F# it also has some drawbacks: any errors you make are not discovered until you generated F# code and tried to compile it.



In the second part of the article, to remedy the problems in the first approach, you need to develop a computation expression notation for building UI code directly in F#. Besides retaining a succinct declarative style, this also has the added benefit of all code being in F#—so you get type safety and the discovery of errors is instantaneous.

Notepad: A Simple WinForms Application

 
Figure 1. Notepad Application: Here is the UI of a simple Notepad application.
Figure 1 shows the UI of a simple Notepad application—basically a form with a menu bar—and an editor control that allows the user to edit text. Fundamentally, even the most complex UIs are built using the very same forms and a breadth of user controls. However, building up the hierarchy of these controls in code is repetitious and error-prone. Consider the following simple example:

#light open System open System.Windows.Forms [<STAThread>] let _ = let form = new Form(Text="Example", Width=400, Height=300) new Button(Text="Click me...") |> form.Controls.Add new Button(Text="And me...", Left=80) |> form.Controls.Add form |> Application.Run

Here, even though you exploit the handy F# shorthand notation for creating a form, and two buttons, and initialize their key properties in one step, you have to manually nest the controls to build the main form before you can run it as an application. This extra bit of work is more apparent and tedious if you had to build a larger collection of controls, say a menu bar with a handful of main menu items and their nested sub menus. Wouldn't it be nice to declaratively express the parent-child relationship that exists between these UI controls? Well, that is not that difficult as you will see shortly.

Part 1: Formgen, A DSL for Declaring UIs

If you had a tool to declare and generate your UI layer, you want it to support the following:
  • The ability to name certain controls
  • The ability to set control properties
  • The ability to nest controls into other controls (containers)

Listing 1 shows an example (Notepad.form) of a DSL that is able to support all the above.

This snippet defines two forms, an empty one (AboutForm) and a more complex main form (MainForm). In this DSL, you can introduce a new control by the plus (+) symbol, give its type (for instance, Form) and name (for instance, MainForm) followed by an optional set of constructor arguments (for instance, mFile("&File")), and an optional set of property assignments in the form of <minus><property><space><value> (for instance, -Width 400). Property values can be of the usual types, with a "code" type being something extra—this can be given inside brackets as a string (for example, look at the shortcut properties of most menu items). Child controls can be nested inside the block delimited by braces, with an optional with <property> prefix that lets you designate the parent control property to which the child nodes are added (this property should support the Add method to add a new item). By default, this property is assumed to be Controls.

Occasionally, you need the ability to specify that certain child controls are assigned to a different property of the parent control—this you can do with an optional as <property> suffix after giving the control's name. For example, the MainForm form has two child controls: a MainMenu control that is added as the form's Menu, and a Panel that is added to the form's Controls list.

Each control in this DSL is named, but there are times when a given name is irrelevant. For example, above you used the same name for separator menu items. By using +: to introduce a new control, you can essentially cause it not to be exposed, thus its name becomes irrelevant.

Furthermore, as you might have spotted, the Click event handler for the exit menu item: by using ++<event-name> you can introduce a new event handler for the parent control and specify its code as a string.

Without further due on the syntax, you can proceed to create an AST definition to hold the necessary information for the above DSL. Your FormType.fs file looks like the following:

#light namespace IntelliFactory.Tools.FormGen module Types = type comp = | Component of bool * string * (string * par list) * string option * prop list * string * comp list | EventHandler of string * string and par = | IntPar of int | FloatPar of float | StringPar of string | BoolPar of bool and prop = | IntProp of string * int | FloatProp of string * float | StringProp of string * string | BoolProp of string * bool | CodeProp of string * string

Here, the comp type defines a component or an event handler. A Component carries a Boolean flag that determines whether its name is exported, its type as a string, its name, and a list of constructor parameters (defined as par objects), an optional name for the as clause, a list of properties (defined as prop objects), the name of the container property to which child controls are added, and a list of child controls and event handlers. Event handlers have a name and a code block defined as a string.

The lexer (FormLexer.fsl) follows a "typical" FsLex definition, except that you added the ability to lex comments and strings with proper escape characters. You can easily add position information also to each token to recover from later semantic errors with an exact position; this was not added in the implementation as shown in Listing 2.

The parser (FormParser.fsy) is equally straightforward and without much explanation you can find it in Listing 3.

At this point, all you have left is a pretty printer that outputs F# code for an AST in your DSL. Here is FormPrint.fs:

#light namespace IntelliFactory.Tools.FormGen module Print = open System.IO open Types exception DuplicateName of string let rec collect_exports env = function | Component (export, ty, (id, pars), _, props, _, comps) -> let env' = if export then if Set.exists (fun (id', _) -> id = id') env then raise (DuplicateName id) Set.add (id, ty) env else env List.fold_left (fun env comp -> collect_exports env comp) env' comps | EventHandler _ -> env

Here, collect_exports takes a component, returns all exported names from it and all of its child nodes, and rejects any duplicate names. This is because for each top-level component, you generate a record type with the exported names as labels. The rest of the file contains various functions for pretty printing (see Listing 4).

Now you can tie it all together: in the main F# module (formgen.fs) call the lexer/parser with an input file, and pretty print the result into an F# code file (see Listing 5).

 
Figure 2. References: Your F# project should contain all of the files and references shown here.
For this, your F# project should contain all of the above files and references as shown in Figure 2. To obtain the lexer and parser implementation (FormLexer.fs and FormParser.fs, respectively) from FormLexer.fsl and FormParser.fsy you need to run:

fsyacc FormParser.fsy fslex FormLexer.fsl

Both fsyacc and fslex are in the F# distribution (in the bin folder).

This completes the formgen tool. To test it, take the Notepad snippet you saw earlier and run the formgen tool on it to generate Notepad.fs. Then add this file to a new F# project, add references to System.Drawing and System.Windows.Forms, and create a new F# file with the following:

#light open System.Windows.Forms open System [<STAThread>] let _ = let form = Notepad.CreateMainForm () form.mAbout.Click.Add (fun _ -> Notepad.CreateAboutForm().AboutForm.Show()) form.MainForm |> Application.Run

This code uses the generated Notepad module to create the main and about forms, and shows how to register an event handler for displaying the about box. You can add further event handlers to fill the Notepad UI with full functionality in a similar fashion.


Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap