Saturday, December 25, 2010

F# GUI plumbing with reactive Events

My most recent "Aha!" moment with the language is to finally wrap my head around the standard Event module -- a sort of lightweight subset of the Reactive Extensions for .net -- while starting to do some more GUI programming in F#, which is making belated use of the GTK#/Glade spikes I made about a year ago.

It may not make much difference in the simplest cases where

ok_btn.Clicked += new EventHandler(OnButtonClick);
view raw gistfile1.cs hosted with ❤ by GitHub

naively went to

ok_btn.Clicked.AddHandler(fun source arguments -> OnButtonClick source arguments)
view raw gistfile1.fs hosted with ❤ by GitHub

but which is equivalent to, using the Event module,

ok_btn.Clicked
|> Event.add (OnButtonClick ok_btn)
view raw gistfile1.fs hosted with ❤ by GitHub

At this point the main difference is the change in the function signature -- first, it is a real F# function rather than being forced to use the implicit cast from a fun to a delegate, even if the function signatures look compatible on the surface; second the function passed to Event.add doesn't have the source argument (but, as shown, that can be brought in as a closure).

So that's a small win from the beginning -- but that just scratches the surface. Where it actually shines is where multiple sources of events have to be handled in much the same way. For example, given a menu containing recently accessed files, with selection meaning to re-open that file, as if they had been selected from a file open action

// Do something only if we actually pick a file
// Use the built-in identity function as a 'T -> 'U option mapper here
// for the case 'T being a FileInfo option and 'U being FileInfo
let click = handler.openButton.Clicked
|> Event.map (fun a -> (*
open a file dialog,
get a FileInfo option from the selected file name;
where cancel => None *))
|> Event.choose id
// Selecting an item from the menu
let select =
handler.openMenu.AllChildren
|> Seq.cast<MenuItem>
|> Seq.map (fun (i:MenuItem) -> i.Activated
|> Event.map (fun a -> new FileInfo((i.Child :?> Label).Text))
|> Event.filter (fun (i:FileInfo) -> i.Exists))
// The sum of all these events
let accumulation = select
|> Seq.fold Event.merge click
// do things with a file selection event
accumulation |> Event.add UpdateRecentFileList
accumulation |> Event.add LoadFile
...
view raw gistfile1.fs hosted with ❤ by GitHub

All the inhomogeneity of the event sources (button click, menu item activation) and their associated EventArgs types is smoothed away by appropriate Event.map calls and the use of closures; different validations are handled by appropriate Event.filter or Event.choose invocations; and then the whole ensemble is gathered together by an appropriate fold with Event.merge as the accumulator.

True, there is nothing here that could not be achieved by making old-style handler functions for AddHandler from equivalent appropriately composed function chains -- the path I was starting to take for this bit of plumbing before I was indirectly reminded of this feature, and dug up this series of posts from a couple of years back (so slightly dated in the details). The big difference that using the Event module makes is that it is more expressive of intent, and separates the concerns -- general event handling plumbing vs this application's business logic -- explicitly.

1 comment :

Art said...

Hi Steve. It's another Dec 25th (almost) ... finally I found your very helpful blog post. Thanks