It’s a curious thing about our industry: not only do we not learn from our mistakes, we also don’t learn from our successes.
—Keith Braithwaite, Software Engineer
The Power of Classes
F# classes give you the full power of object oriented programming. When you need to go beyond record types, for example, when the external and internal representations of data need to differ, or when you need to hold or even mutate state over time, classes are often the answer. They are also a great solution when you need to interact closely with an OO codebase, for instance, by participating in a class hierarchy. F# classes can inherit from C# classes and can implement C# interfaces, and vice versa.
By the way, I’m avoiding using the phrase class types (although that is what we are talking about), because there also exists a rather different concept, confusingly called type classes. Type classes aren’t supported in F# at the time of writing, although there is work going on in that area so perhaps someday they will be. I’m not going to talk about them at all in this book, and I’ll stick to the term class for object oriented F# types and their C# equivalents.
Asymmetric Representation
One place where you may want to use a class is where the internal and external data representations need to be asymmetric. In other words, the set of values you see when you use an instance isn’t the same set you provided when you created the instance.
A console input prompt class
Note
In Listing 8-1, I’ve used printfn rather than printf to output the prompt message. This is simply because F# Interactive doesn’t work well with printf.
ConsolePrompt is a great example of asymmetry: when you create an instance, you provide a prompt message, but there is no property that lets you get that message back. (Why would there be? It's sufficient for that message to be printed out when prompting for input.) Conversely, the class’s one member, GetValue(), returns something that is independent of anything that was provided on construction, because what it returns was typed in by the user.
The class declaration (up to any member definitions) and the primary constructor are one and the same thing. In a moment we’ll say how to do things in the constructor body – in this first example, there isn’t really a body at all.
Values provided in the constructor arguments (in this case, message) are available throughout the rest of the class. Thus when the input prompt is printed, we can access the message value without further ceremony. Constructor arguments don’t have to be exposed as members, though they can be, if their values need to be accessible outside the class definition.
Members are declared simply by saying
member <self-identifier>.Name(<parameters>) = <body>
The self-identifier, in this case this, can be any non-reserved identifier, though you might as well stick to this if you are going to use it in the member body, as we do here. People often use __ (double underscore) as the self-identifier if they aren’t going to reference the current instance in the member body. This is just a convention to avoid having obvious unused identifiers, and doesn’t affect behavior.
It’s also interesting to note that members can call themselves recursively, as we do here when no correct input is entered. You don’t have to use any special keyword like rec, as you do when making stand-alone functions recursive.
Constructor Bodies
A class with code in its constructor
There are two kinds of operations you can conduct in a constructor body. One is imperative operations: actions that do something but which don’t return anything. Imperative actions commonly performed here include validation of the constructor arguments, and possibly logging. In this case we validate the prompt message to make sure it isn’t null or blank. Imperative actions in F# class constructor bodies must be contained within a code block beginning with the keyword do .
The other kind of thing we can do in the constructor is to bind values using the let keyword. In Listing 8-2 we bind a new value for message, containing the original message value but with any leading or trailing spaces trimmed. This is an example of shadowing, not mutation. The original value for message isn’t overwritten, it’s just hidden because there is a new thing in the same scope with the same name. In this case, because the cleaned-up message is the definitive version, and we don’t want any later code to access the original input, it’s reasonable to use the same name. Again, as with constructor arguments, values bound with let in the constructor body are available throughout the body of the class, including in all its methods. You don’t use a self-identifier like this to access them, as they aren’t properties.
Values as Members
Values as members
Here we publish the cleaned-up version of message as a property called Message. Callers can access the value using dot notation. No brackets are needed after the definition or call, because it’s a read-only property that simply gets the value of message, which itself never changes.
Getters and Setters
Adding a mutable property with member val and default getter and setter
Using a so-called auto-implemented property is a reasonable thing to do here: BeepOnError is not an important enough thing to have as a constructor parameter, and a default value is fine; but one would also like to have the flexibility to change it. And as a Boolean it hardly needs validation!
Additional Constructors
An additional constructor
In Listing 8-5 we’ve added a beepOnError parameter to the primary constructor, and accessed its value in GetValue() when deciding whether to beep. Then we’ve added an additional constructor after the body of the primary constructor (i.e., after the validation and trimming of message). Secondary constructors are always declared using the new keyword (you don’t use the name of the class), and must always call the primary constructor. In this case we call the primary constructor with whatever message value was passed in, and a hardwired default value of true for beepOnError.
Additional constructors are useful, but having more than a very small number of them may be a code smell. If you need so many permutations of construction values, does your class really have a “single responsibility,” as it should?
Explicit Getters and Setters
Explicit getters and setters
Note
For simplicity, in Listing 8-6 I’ve omitted some of the features we added to ConsolePrompt in previous listings.
There are mutable backing stores to store the current state of the foreground and background colors. We declare these and bind their initial values in the class constructor.
The getter has a unit parameter (), and a body that returns something: in this case a tuple of the currently set colors.
The setter has a parameter that is used to provide a new value. In this case it’s a tuple of colors. The setter body validates that the new colors are different, and if so updates the mutable backing values.
Note
You can test out the validation in F# Interactive, but if you want to see the colors in action, you’ll have to incorporate the Listing 8-6 code into a console program. At the time of writing, F# Interactive doesn’t support console colors.
Internal Mutable State
A class with internal mutable state
There’s nothing inherently evil about a mutable state, especially, as here, where nothing else can directly see or change it. That said, you do have to be a bit canny about thread safety. It’s hardly likely in the case of ConsolePrompt, but what would happen if two threads were to call GetValue() and both happened to check attempts < maxAttempts simultaneously? Both might pass the check before either of them incremented attempts. Both threads would then perform the recursive GetValue() call, meaning that the total attempts could exceed the specified limit. Whenever there is mutation, you should always consider thread safety, even if it’s only to document “this method is not thread safe.”
Generic Classes
Then we need to think about how to convert from what the user enters into the type we want (string, integer, floating point, or whatever). We also need to take into account that the user might not type in what we expect: if they type “abc” or just press Enter, we won’t be able to convert that into an integer, so we must build in the possibility of failure.
(I’ve kept the maxAttempts parameter from the previous section as it’s relevant to how we’ll need to use the converter.)
Get the user input.
Pass it to the supplied conversion function.
If that succeeds, return the converted value.
If it fails, increment attempts and if the maximum isn’t exceeded, try again.
Using an injected conversion function
It’s interesting to note that we don’t need to change the signature of GetValue to make it return 'T rather than a string. The fact that it uses tryConvert to generate a return value, and that tryConvert returns 'T, is enough for type inference to do its job.
Using the generic ConsolePrompt class
The GetValue function using Option.defaultWith
This works as well as Listing 8-9, but I personally think it is slightly less readable. On the other hand, if one needed to perform additional operations on the tryConvert result, then there would be more of a case for using Option.defaultValue, as the functions in the Option module pipe together nicely.
Named Parameters and Object Initializer Syntax
Alternative construction styles
The namePrompt1 instance is constructed in the default manner, giving arguments in the same order that the parameters are declared in the constructor. To work out what the arguments mean, you’d have to guess from their values or look at the class definition. (Or if you are lucky, the author may have documented the parameters in a /// comment, meaning that the definitions would appear in a tool tip in most IDEs.)
The namePrompt2 instance is constructed using named argument syntax. It’s a little more verbose but much more readable.
The namePrompt3 instance assumes that the ConsolePrompt class has the BeepOnError mutable property we introduced in Listing 8-4. You can set this at construction time using object initialization syntax . Just include assignments for mutable properties after you’ve provided values for all constructor arguments, all within the same pair of brackets. Here we do this without using named argument syntax (namePrompt3)
The namePrompt4 instance combines named argument syntax with an object initializer syntax.
In my opinion, named argument syntax isn’t used enough in F# code. Object initialization syntax is more well-used, particularly when interacting with C#-based APIs, which tend to make extensive use of mutable properties. In fact, I would say: if you find yourself creating instances and immediately setting their properties, always change this to object initializer style.
Make sure you give some thought to which style you adopt. Naming at the call site can be very helpful to the reader, especially when you are calling APIs that require you to set lots of constructor arguments and mutable properties.
Indexed Properties
A ring buffer implementation
The important part here is the member called Item. When a property has the name Item and an index argument (here we used i), it describes a read-only, indexed property that can be accessed by the caller using array-like syntax. Notice that I used the name _items for the array backing store. I could instead have shadowed the original items sequence by reusing the name items for the backing store array. I used a private backing store to avoid potentially slow indexed access into the provided sequence.
Settable indexed properties
A two-dimensional ring buffer
The one subtlety here is that the dimension index parameters (i and j in this case) must be tupled together. But the value parameter in the set declaration is curried – that is, it’s outside the brackets that surround i and j.
Interfaces
Interfaces are a key concept of the Object Oriented world. An interface lets you define a set of members in terms of their names and type signatures, but without any actual behavior. Classes may then implement the interface, in other words, provide member implementations that have the same names and type signatures as the members defined in the interface.
Simple interface definition for a media player
Listing 8-15 starts with a couple of type aliases, which give new names for the string and int types. I’m not a huge fan of littering code with type aliases, but when defining interfaces, they make a lot of sense. They help motivational transparency by incorporating meaningful names for both parameters and results in the type signature. Next we have a Discriminated Union called Status, which embodies the states that the media player can be in. Finally, there is the actual interface definition. It starts with type <Name>, just like a class definition, but since there can be no constructor, the name isn’t followed by brackets or constructor parameters.
The definition of the interface consists of a series of abstract member definitions, each of which must have a name, such as Open, and a type signature, such as MediaId -> unit. Use the keyword unit if you need to express the fact that the member doesn’t require any “real” parameters, or that it doesn’t return anything “real.”
The beauty of this approach is that you can start to think about the design of your classes (the ones that will implement this interface) without getting distracted by implementation details. For example, the fact that Open has a signature of MediaId -> unit tells you that implementations of Open aren’t going to return any feedback to the caller about whether they successfully opened the requested media item. This further implies that any failures are either swallowed and not reported back, or are signaled in the form of exceptions. That might or might not be the best design: the point is that you can think about it here, before committing to a lot of coding in either the implementation of classes or their consuming code. If you have become accustomed to designing systems based on function signatures, you could think of an interface as a sort of big, multi-headed function signature.
Implementing an Interface
Implement the interface by adding interface <Interface Name> with, followed by implementations for each member in the interface. Each member declaration replaces the abstract member from the interface with a concrete member containing real code. The implementation needs to have the same function signature as the interface member it is implementing.
In Listing 8-16 I’ve called the implementation DummyPlayer because this class doesn’t really do very much – it certainly doesn’t actually play media! But you can see how a real implementation would fit into the same interface/implementation pattern.
Accessing interface members
To help me remember which operator to use, I visualize the upcast operator :> as a sort of sarcastic emoticon, saying “Haha, you forgot to cast to the interface… again!”
Accessing instance and interface members
The class in Listing 8-18 implements two interfaces: IMediaPlayer and IDisposable , and it also creates a memory stream in the class constructor, just as an example of a resource that the class might want to dispose promptly when it itself is disposed. It also has a member of its own, called UniqueId. In the demo() function, we create a player, open a media item, then explicitly dispose the player. (By the way it would be better generally to create the player with the use keyword or the using function, meaning that the player instance would be disposed on going out of context. I’ve coded in this way so you can see the casting in action.)
Notice how, to call the Open() method, we cast to IMediaPlayer; to call the Dispose method we cast to IDisposable; and to use the UniqueId property we don’t cast at all, because this is a member of the class itself. You might wonder why I didn’t have to cast the stream instance to IDisposable when calling its Dispose() method. The answer is that the C# code for MemoryStream didn’t implement the IDisposable interface explicitly. F# always implements interfaces explicitly; in C# you have the choice.
Object Expressions
You can use an object expression to create a “something,” which inherits from a class or implements one or more interfaces, but which is not a new named type. It’s a great way of creating ad hoc objects for specific tasks, without actual, named types proliferating in your codebase.
Using Object Expressions
In Listing 8-19 we make a new implementation of IMediaPlayer that requires a logger as a constructor argument. The logger needs to be of type ILogger, which I’ve also declared here but could just as easily be defined externally. The LoggingPlayer implementation calls the logger’s Info and Error methods at various points.
The object expression part comes in the demo() function, where we create a value called logger that implements ILogger but is not a new, named type. The curly brackets {} are important here: they are part of the object expression. When various members of the LoggingPlayer instance are called, these call the methods we defined in the logger binding.
I personally don’t use object expressions very often, but they can certainly be useful. The times I have used them have been in writing tests in F# for highly coupled C# codebases. There they really have been a boon.
Abstract Classes
A method is abstract if it is marked with the keyword abstract, meaning that it can be overridden in a derived class.
Abstract members can have default definitions, meaning that they can be, but don’t have to be overridden.
A class is only considered abstract if it contains at least one abstract member that doesn’t have a default implementation. Classes that fall into this category must be annotated with the [<AbstractClass>] attribute.
Thus a class’s members can all be abstract without the class being considered abstract: that’s when all the class’s abstract members have default implementations. Classes that fall into this category must not be annotated with the [<AbstractClass>] attribute.
Confused yet? Let’s look at some examples.
Abstract Members
A simple abstract class
So don’t forget the attribute!
Default Member Implementations
Default abstract member implementation
See how SaySomething is defined twice in ParentClass, once as an abstract member and again as the default implementation of that member.
It’s important to notice that, because SaySomething now has a default implementation, its class is no longer considered abstract. This is why I’ve renamed the class ParentClass and removed the [<AbstractClass>] attribute. It would still be abstract if there was at least one other abstract member that didn’t have a default implementation.
Moving on to the derived classes: one of them, ConcreteClass1, doesn’t override SaySomething, so the default implementation takes over. The other one, ConcreteClass2, does override SaySomething, and it is the overriding implementation that we see in operation.
Class Equality and Comparison
Although many F# developers don’t use inheritance or interfaces a great deal, there are a few standard interfaces that we often have to support. One is IDisposable , which we dealt with briefly above. The others are IEquatable and IComparable , which are used to determine if two instances are equal in some meaningful sense, and whether one is larger or smaller than another.
Implementing Equality
Two identical geographical positions might be “unequal”
Note
Comparing floating-point values is always a dangerous thing to do: two GPS positions might only differ by the width of an atom and still be considered different on the basis of floating-point comparison. To keep things simple, I’m going to ignore that aspect and assume that instances like landsEnd and landsEnd2 come from some completely repeatable source. I’m also ignoring what goes on at the North and South Poles, and what happens if someone sends in an out-of-range value like 181.0 for longitude!
In the world of classes, the standard way to represent equality is to implement the .NET interface IEquatable . There are some gotchas in doing this though, so I’ll take it a step at a time and make a few deliberate mistakes.
Just implementing IEquatable isn’t enough
Overriding Object.Equals
The [<AllowNullLiteral>] attribute has been added to the class. This allows other languages to create null instances. If we are implementing LatLon as a class instead of an F# record, this is a likely use case.
There’s now a private eq function that does the real work of comparing instances. This includes a null check, which is now necessary as a result of adding [<AllowNullLiteral>].
We’ve overridden the Object.Equals() method. Since this takes an obj instance as its argument, this needs to pattern match on type before calling eq.
The IEquatable implementation also calls eq.
We’ve also overridden the Object.GetHashCode() method.
If two objects are considered equal they must have the same hash code. (Within one Application Domain that is – the underlying hash code generator can vary between platforms and versions.)
If two objects are considered not equal, they will usually have different hash codes, though this is far from guaranteed.
The purpose of hash codes is to provide a quick way to check for likely equality in, for example, dictionary implementations. You wouldn’t normally use hash codes directly – unless you were implementing some special collection type of your own – but you are encouraged to override GetHashCode so that your class can be placed into hash-based collections efficiently. Luckily this is often easy to do: just tuple together the items that embody equality (in this case, the latitude and longitude values) and apply the built-in hash function to them, as we did in Listing 8-24.
Exercising equality
See how we use LatLon instances as keys in a dictionary. (The dictionary values here are integers and aren’t meaningful beyond being something to put in the dictionary.) The first instance (landsEnd, i=0) isn’t represented when we print out the dictionary contents, because it was replaced by another item with the same key (landsEnd2, i=2).
Overriding op_Equality
Add the lines from Listing 8-26 after the latitude and longitude members. op_Equality is a static member. As in C# this means that it isn’t associated with any particular LatLon instance.
Implementing Comparison
Sometimes you get can away with only implementing equality and not comparison, as we did in the previous section. In fact, it doesn’t seem particularly meaningful to implement comparison (greater than/less than) for LatLon instances. Which is genuinely “greater” – LatLon(1.0, 3.0) or LatLon(2.0, 0.0)? But there’s a catch: some collections, such as F# sets, require their elements to be comparable, not just equatable, because they rely on ordering to search for items efficiently. And, of course, other classes might have an obvious sort order, which you might need to implement, so it’s important to know how.
Implementing IComparable
Now that you’re familiar with interfaces in F#, this code should be pretty self-explanatory. We implement IComparable and implement its one method: CompareTo() . Then, in a similar way to the Equals() override, we use pattern matching on types to recover the other LatLon instance. We take the latitudes and longitudes from the instances being compared, and pass them as tuples to the built-in compare function, which will do the real comparison work for us. Pleasingly, using compare means we don’t have to worry about whether, for example, (50.07, -5.72) is less than or greater than (58.64, -3.07). Whatever the compare function does for us is going to be consistent.
Using class instances that implement IComparable
Adding a generic version of IComparable
As with equality, I’ve moved the implementation of comparison to a private function called comp, and delegated to that from both the IComparable and the new IComparable<LatLon> implementations.
Now you know why F# record types, with structural equality and comparison by default, are so valuable! If you even dip a toe into equality or comparison for classes, you pretty much have dive into the pool completely. Sometimes that’s worth it, sometimes not.
Recommendations
Use F# classes when the modeling possibilities offered by simpler structures, such as F# records and Discriminated Unions, aren’t sufficient. Often this is because there is a requirement for asymmetric representation: the type is more than just a grouping of its construction values, or it needs to have moving parts.
Also use classes when you need to participate in a class hierarchy, by inheriting from, or providing the ability to be inherited from. This is most common when interacting with C# codebases, but may also be perfectly legitimate in F#-only codebases in cases where class hierarchies are the easiest way to model the requirement.
Be aware of the benefits and costs of going down the OO route. Don’t just do it because you happen to have more experience in modeling things in an OO way. Explore the alternatives that F# offers first.
Don’t forget the power of object expressions to inherit from base types or implement interfaces without creating a new type.
All the major OO modeling facilities offered by C# are also available in F#: classes, abstract classes, interfaces, read-only, and mutable properties – even nullability.
Summary
The chapter has two messages. The explicit message is “Here’s how to do Object Orientation in F#. Here’s how to write classes, use interfaces, override methods and so forth… .” The implicit message is “Object Orientation can be a slippery slope.” Compare, for example, what we ended up with in Listing 8-29 with what would have been achieved, almost for free, using an F# record. (Accepting the dangers and limitations of comparing floating-point values, which apply to both the class and the record version.) Also, it’s interesting to note that this chapter is the longest in the book, and was by far the hardest to write. It’s hard to be concise when writing classes, or writing about classes!
The OO philosophy sometimes feels as though it involves taking something complicated and making it more complicated. (I’m indebted to Don Syme, “father of F#”, for this phrase.)
OO code can be harder to reason about than a good, functional implementation, especially once one opens the door to mutability.
OO code tends to embrace the concept of nullability, which can complicate your code. That said, as we discovered in Chapter 3, the introduction of nullable reference types into C# may change the balance of power here.
.NET is fundamentally an OO platform. This isn’t just built into the C# language – the lower level IL into which both C# and F# is compiled is also inherently object oriented. This fact can leak into your F# code, and frankly you shouldn’t waste too much time fighting it.
Many of the Nuget packages and other APIs you will be coding against will be written in terms of classes, interfaces, and so forth. Again, this is just a fact of life.
The OO world has an immense depth of experience in building working, large-scale systems. A dyed-in-the wool F# developer like me would argue that these systems have often not been built in the most productive way. But there is no denying there have been many successes. It seems foolish to dismiss all this hard-won knowledge.
So that you can make informed design decisions, make sure you are familiar with the basics of F# classes, constructors, overrides, interfaces, and abstract classes. Don’t forget how useful object expressions can be for making ad hoc extensions to a class without a proliferation of types. Above all, be extremely cautious about implementing deep hierarchies of classes. I’ve rarely seen this turn out well in F# codebases.
In the next chapter we’ll return to F# fundamentals and look at how to get the best out of functions.
Exercises
Exercise 8-1 – A Simple Class
Make a class that takes three byte values called r, g, and b, and provides a byte property called Level, which contains a grayscale value calculated from the incoming red, green, and blue values.
The grayscale value should be calculated by taking the average of the r, g, and b values. You’ll need to cast r, g, and b to integers to perform the calculation without overflow.
Note
This is a terrible way to calculate grayscale values, and probably a terrible way to model them! The focus of this and the next few exercises is on the mechanics of class definition.
Exercise 8-2 – Secondary Constructors
Add a secondary constructor for the GrayScale class from Exercise 8-1. It should take a System.Drawing.Color instance and construct a GrayScale instance from the color’s R, G, and B properties.
Exercise 8-3 – Overrides
Exercise 8-4 – Equality
Implement equality for the GrayScale class by overriding GetHashCode() and Equals(), and implementing the generic version of IEquatable. The GrayScale class should not be nullable (don’t add the [<AllowNullLiteral>] attribute).
Prove that GreyScale(Color.Orange) is equal to GreyScale(0xFFuy, 0xA5uy, 0x00uy).
Prove that GreyScale(Color.Orange) is not equal to GreyScale(Color.Blue).
What happens if you check equality for GreyScale(0xFFuy, 0xA5uy, 0x00uy) and GreyScale(0xFFuy, 0xA5uy, 0x01uy). Why is this?
Exercise Solutions
Exercise 8-1 – A Simple Class
Exercise 8-2 – Secondary Constructors
Exercise 8-3 – Overrides
Exercise 8-4 – Equality
GreyScale(0xFFuy, 0xA5uy, 0x00uy) is equal to GreyScale(0xFFuy, 0xA5uy, 0x01uy) even though the input RGB levels are slightly different. This is because we lose some accuracy (we round down when doing integer division) in calculating Level to fit into a byte range (0..255), so certain different combinations of inputs will result in the same Level value.