Object Oriented F# - Encapsulation with Object Expressions
In the past, I've covered a bit about object oriented programming in F#. I'd like to come back to that series as there is much yet to cover on this topic. Last week, I spent some time with Erik Meijer at QCon and he and I both agreed that in some ways, F# is a better object oriented language than C# in some ways given some of the language flexibility.
Let's get caught up to where we are today:
Today's topic will be on object expressions and encapsulation in general.
Encapsulation
When we think of object oriented programming, encapsulation is a fundamental technique. This is to hide design decisions that are likely to change behind well-defined boundaries, and thus protecting parts of the program from that change. The benefits we get is that we can evolve our implementations over time without affecting other parts of our system. It's a pretty elementary topic and yet deeply important.
In F#, we have a couple of options we can talk about with regards to encapsulation which are object expressions and accessibility notations, the latter of which will be covered in a later post.
Object Expressions
One of the simplest ways of encapsulation in F# is to make them local to expressions or class definitions using the inner let bindings. These values are not directly accessible from outside the scope. Typically we use these to hold some sort of state which then is encapsulated inside implementation of our classes or expressions.
Let's look at a simple implementation of a counter that encapsulates state. We can encapsulate the state of the current count and then increment and have it keep count of our current state. Let's look at how we might do that in F#.
let counter =
let count = ref 0
fun () -> incr count; !count
[<Fact>]
let counter_should_increment() =
let c1 = counter()
let c2 = counter()
Assert.Equal(c1, 1)
Assert.Equal(c2, 2)
You'll notice the use of the ref keyword which indicates that in order to properly be accessed and modified inside a closure, it cannot be mutable, but only a reference cell. Brian McNamara of the F# team recently covered this on a blog post about the F# ref type and On lambdas, capture and mutability. We also use the incr function which adds one to the reference cell. What's important to realize here is that the count reference cell is never exposed to the outside world, only its value through the function.
Object expressions are another interesting part of F#. This allows me to define local implementations of a given type and return it inline in a function, method or otherwise. Let's look at a few examples. The first one would be to create a comparer with two companies based upon size.
type Company = { Name:string; Size:int; }
let companySizeComparer =
{ new System.Collections.Generic.IComparer<Company> with
override x.Compare(c1, c2) =
c1.Size.CompareTo(c2.Size)
}
[<Fact>]
let companySizeComparer_ShouldCompareSizes() =
let c1 = { Name = "Microsoft"; Size = 70000 }
let c2 = { Name = "Google"; Size = 20000 }
let c = companySizeComparer.Compare(c1, c2)
Assert.Equal(c, 1)
Another example would be to create a Windows Form with an onload event prepopulated. Note that is is a pretty naive example of how you would use it, but it shows how you might override items in a given class.
open System.Windows.Forms
let helloform =
{ new Form() with
member x.OnLoad(args) =
base.OnLoad(args)
MessageBox.Show("Hello World!") |> ignore
}
One last example I'll show is the simple map2 implementation from the F# libraries. This gives me the ability to combine two collections as one, which should be identical to the new implementation of the Zip function in the new .NET 4.0 BCL. I can create a simple IEnumerator<T> to encapsulate two collection iterators so that I can iterate over both at the same time and perform my calculation on each item.
{ new IEnumerator<'c> with
member x.Current = f e1.Current e2.Current
interface IEnumerator with
member x.Current = box (f e1.Current e2.Current)
member x.MoveNext() =
let n1 = e1.MoveNext()
let n2 = e2.MoveNext()
n1 && n2
member x.Reset() = e1.Reset(); e2.Reset();
interface System.IDisposable with
member x.Dispose() = e1.Dispose(); e2.Dispose() }
To do something similar in C#, I'd have to go through the pomp and circumstance of having to create a full class, and then an implementation to then iterate over each, such as this:
{
private readonly IEnumerator<T> e1;
private readonly IEnumerator<U> e2;
private readonly Func<T, U, V> f;
public SequenceMapEnumerator(
IEnumerator<T> e1,
IEnumerator<U> e2,
Func<T, U, V> f)
{
this.e1 = e1;
this.e2 = e2;
this.f = f;
}
public V Current
{
get { return f(e1.Current, e2.Current); }
}
public void Dispose()
{
e1.Dispose();
e2.Dispose();
}
object System.Collections.IEnumerator.Current
{
get { return f(e1.Current, e2.Current); }
}
public bool MoveNext()
{
var n1 = e1.MoveNext();
var n2 = e2.MoveNext();
return n1 && n2;
}
public void Reset()
{
e1.Reset();
e2.Reset();
}
}
And then the implementation of the map function might look like this:
this IEnumerable<TArg1> arg1,
IEnumerable<TArg2> arg2,
Func<TArg1, TArg2, TResult> func)
{
var e1 = arg1.GetEnumerator();
var e2 = arg2.GetEnumerator();
var s = new SequenceMapEnumerator<TArg1, TArg2, TResult>
(e1, e2, func);
while (s.MoveNext())
yield return s.Current;
}
Much like we did above, we could also encapsulate internal state inside of our object expression. What we're allowing is just a simple session value holder for one item, and then implementing a counter using this technique.
abstract member Get : unit -> 'a
abstract member Put : 'a -> unit
let counter initialState =
let state = ref initialState
{ new ISession<int> with
member x.Put(value) =
state := !state + value
member x.Get() =
!state
}
So, as you can see, there's a bit to this and can make your code a bit more compact with local type definitions. In some ways, with features like these, F# can be more object oriented than C#.
Conclusion
One of the more interesting object oriented features in F# is the object expression, and some associated techniques for encapsulation. With the conciseness of the language, we are able to create local definitions of classes and interfaces to return from functions. As the C# language evolves, we may start to see features such as this creep into the language much as other functional programming items have in the C# 5.0 timeframe. But, I hold in my opinion that F# can be a better object oriented language, in part due to the fact that void is actually treated as a real type, which is a major problem in other languages such as C#. With this, I can then really generalize my class implementations and not have to worry about what my return type may be, if any at all.
There's a lot more to cover in regards to object orientation in F#, including more of the back to basics approaches.