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# : Page 2

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


advertisement

Part 2: Using Computation Expressions to Model Building UIs

While the formgen tool can save you significant time when building complex UIs, it suffers from providing no feedback on correctness until the generated file is compiled. Ideally, it is best to retain the same shortness and declarative style of definition but as F# code. That way, semantic errors would surface as you type the form definition in Visual Studio and you can call IntelliSense for help, etc.

If you study the formgen generated code, you can see that most of the gain you get by using it is in generating the code for adding child controls to parent containers. You can use computation expressions in a clever way to get the same benefit, say by overriding the let! construct in your own UI control builder objects. So it would look something like:

let form = formM() { let! panel1 = new Panel() let! panel2 = new Panel() return () }

Here you have a form builder object (an instance of the formM type) and each let! binding creates a new child control. You can then build similar builder objects for other controls, encapsulating the details of what property is assigned when a new child is added, etc.



Computation Builders

F# computation expressions are a great way to add domain specific functionality to your F# code. By defining a given code block as a computation expression, you allow the code inside that block to be governed by a computation builder object. This is performed by the F# compiler automatically de-sugaring each F# construct in the code block to calling certain methods in your computation builder object. You can define computation builders as normal class types and define how each F# construct is handled by adding the appropriate de-sugared members. Below is a list of F# constructs (defined as cexp, and exp being ordinary F# expressions) and their de-sugared counterparts:

let! pat = exp in cexp b.Bind(exp,(fun pat -> «cexp»)) let pat = exp in cexp b.Let(exp,(fun pat -> «cexp»)) use pat = exp in cexp b.Using(exp,(fun pat -> «cexp»)) use! pat = cexp in cexp b.Bind(exp,(fun x->b.Using(x,(fun pat-> «cexp»)))) do! exp in cexp b.Bind(exp,(fun () -> «cexp»)) do exp in cexp b.Let(exp,(fun () -> «cexp»)) for pat in exp do cexp b.For(exp,(fun pat -> «cexp»)) while exp do cexp b.While((fun ()-> exp),b.Delay(fun ()-> «cexp»)) if exp then cexp1 else cexp2 if exp then «cexp1» else «cexp2» if exp then cexp if exp then «cexp» else b.Zero() cexp1; cexp2 b.Combine(«cexp1», b.Delay(fun () -> «cexp2»)) return exp b.Return(exp) return! exp exp

You do not have to provide all of these methods in a computation builder object. For instance, if you use let! in a computation expression for which the builder object does not have a corresponding Bind member you will get an error from the compiler.

Defining Computation Builders for UI Controls

Most UI controls are descendants of the System.Windows.Forms.Control class, so you can create an interface that all computation builder objects that build these Control descendants have to implement:

open System.Windows.Forms open System.Drawing [<AbstractClass>] type 'a IComposableControl when 'a :> Control () = abstract Self : 'a member self.Return (e: unit) = self.Self member self.Zero () = self.Self [<OverloadID("1")>] member self.Bind(e : Control, body) : 'a = e |> self.Self.Controls.Add body e [<OverloadID("2")>] member self.Bind(e: 'b IComposableControl when 'b :> Control, body) : 'a = e.Self |> self.Self.Controls.Add body e

This interface contains an abstract Self member that you can use later to fetch the actual UI control that was built using the computation builder. These computation expressions are likely to contain let! and let! bindings so you will need a way to "end" the computation expression with a trailing statement. You don't want these expressions to return just any value; you want to actually return the UI control object that is managed by the builder object. Hence, the interface also contains an implementation for the Return and Zero members, requiring that you can only return a unit value, which you use to return the managed control itself.

Your interface also contains two overloaded Bind members that correspond to let! constructs, each taking a direct Control descendant or another IComposableControl builder object, and adding them to the Controls collection of the managed UI control.

Armed with this interface you can go ahead and implement various builders for Control descendants. Below you can find three builder classes that build forms, panels and rich text boxes.

type formM() = inherit IComposableControl

() let c = new Form() override self.Self = c type panelM() = inherit IComposableControl

() let c = new Panel() override self.Self = c type formM() = inherit IComposableControl() let c = new Form() override self.Self = c

Implementing Additional UI Control Builders

There are UI controls that are not Control descendants, and as such they may have a container property different than Controls. One such control is MainMenu and its related MenuItem controls. For these, you create a similar abstraction but wrap the MenuItems container property (see Listing 6).

At this point you can define your Notepad UI as shown in Listing 7.

Speeding Up the Development of UI Code

In this article, you saw two fundamental language-oriented approaches to speed up developing UI code in your F# applications. In the first approach, you defined a DSL, implemented a lexer and parser for it, and wrapped it as a tool to generate F# code from a UI definition in this DSL. The generated files can then be added to your application logic to form a complete application.

In the second approach, you developed various computation builders that manage UI controls and used these builders in nested computation expressions to describe the UI of your application in a succinct and concise way without having to add any code for managing the parent-child relationship between the participating controls. The added benefit of this approach over the first one is that you get instant feedback on your UI's correctness from the compiler, making it an excellent alternative to building UIs in the absence of a Visual Studio form designer that can output F# code, while keeping the entire application code in F#.



Adam Granicz is the CEO of IntelliFactory, the primary place for F# development, training and consulting services, and the coauthor of Expert F#, the most comprehensive guide on F# coauthored with Don Syme, the designer of the language. He has done research in functional programming languages and holds a Masters degree from the California Institute of Technology.
Comment and Contribute

 

 

 

 

 


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

 

 

Sitemap