Chaining Side Effects in F# (and WPF DependencyProperty as a Special Case)
The first question that any functional programmer should ask is, “why are there side effects?!” Side effects are anathema to functional programming purists because they introduce many kinds of undesirable characteristics into code, limiting optimisation and restructuring options. However, most object-oriented libraries, including the .NET BCL, depend on side effects and many of these are intrinsic to writing .NET applications.
WPF is a special case where this applies, with DependencyProperty fields.
A typical example is mutating an object instance by setting the value of a field or property. A pure functional program would return a new immutable object containing the changes and the previous object would be forgotten. However, in the object-oriented world mutating objects is both normative and required. This can be accommodated in F# by using do statements (implicitly or explicitly), but this creates really ugly code:
let f (c:MyClass) = c.MyField1 <- value c.MyField2 <- value
This may not look too bad, but once you get 10 or more of these statements then things are really going wrong! The style is no longer functional and it is difficult to compose these statements because they are sequentially executed. At this point you are writing imperative code in a functional language. It is great that F# allows the mix of functional and imperative styles, but sometimes it would be great to do a little better.
The first key to doing a bit better is to chain functions, for example:
let setField1 value (c:MyClass) = c.MyField1 <- value ; c let setField2 value (c:MyClass) = c.MyField2 <- value ; c let f c = c |> setField1 value1 |> setField2 value2
And the composed function f is now able to be itself composed. Unfortunately, BCL fields and properties are instance members and so we would need to write static functions for every property we want to be able to set. We can manage this using the extension method approach to add static methods to existing classes:
module Extensions =
type MyClass with
static member SetField1 value (self:MyClass) =
self.MyField1 <- value ; self
static member SetField2 value (self:MyClass) =
self.MyField2 <- value ; self
open Extensions
let f c =
c
|> MyClass.SetField1 value1
|> MyClass.SetField2 value2
Unfortunately, there is a problem with this. The result of the methods is of type MyClass but the type of c only has to be ‘a :> MyClass. This means that if you want to pipe c into a method of a subclass then you have to first cast c back to the original type.
We can improve on this by making the static methods generic:
module Extensions =
type MyClass with
static member SetField1<'T when 'T :> MyClass> value (self:'T) =
self.MyField1 <- value ; self
static member SetField2<'T when 'T :> MyClass> value (self:'T) =
self.MyField2 <- value ; self
open Extensions
let f c =
c
|> MyClass.SetField1 value1
|> MyClass.SetField2 value2
This can make for very readable code but it can still be improved in some cases. In WPF, for example, most important control properties are actually modeled by DependencyProperty static fields. These are of a single type and so it is possible to write a single extension method to wrap setting the value of any dependency property on any class:
open System.Windows
open System.Windows.Controls
module Extensions =
type System.Windows.DependencyProperty with
member self.Set<'T when 'T :> DependencyObject> value (depobj:'T) =
depobj.SetValue(self, value) ; depobj
open Extensions
let newWindow title =
new Window()
|> Window.TitleProperty.Set title
|> ...
This approach does not give type checking as DependencyObject.SetValue only expects a System.Object for the value, but it handles all dependency properties and so you can write specific type-safe methods for the more common cases.
I will give further examples of this technique in a future post.
Finally, I believe there is a bug in the May CTP of F# when handling extension methods. If you get a compiler error then send me a message and I will explain the issue. I have logged the issue with the F# product team and I expect it will be resolved before .NET 4.0 is released.
1 comment so far
Comments are closed.
I have had a response from the F# product team and the bug I mentioned is already fixed in their internal development versions.