© Kit Eason 2018
Kit EasonStylish F#https://doi.org/10.1007/978-1-4842-4000-7_8

8. Classes

Kit Eason1 
(1)
Farnham, Surrey, UK
 

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.

Let’s say you want to encapsulate the concept of an input prompt for use in a script or console program. Listing 8-1 shows a simple implementation.
        open System
        type ConsolePrompt(message : string) =
            member this.GetValue() =
                printfn "%s:" message
                let input = Console.ReadLine()
                if not (String.IsNullOrWhiteSpace(input)) then
                    input
                else
                    Console.Beep()
                    this.GetValue()
        let firstPrompt = ConsolePrompt("Please enter your first name")
        let lastPrompt = ConsolePrompt("Please enter your last name")
        let demo() =
            let first, last = firstPrompt.GetValue(), lastPrompt.GetValue()
            printfn "Hello %s %s" first last
        // > demo();;
        // Please enter your first name:
        // Kit
        // Please enter your last name:
        // Eason
        // Hello Kit Eason
Listing 8-1

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.

In case you aren’t already familiar with F# class syntax, let me point out some key aspects of the ConsolePrompt declaration in Listing 8-1.
  • 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

Our initial cut of ConsolePrompt (Listing 8-1) had no constructor body, but you can add one by placing code immediately after the first line of the class definition (Listing 8-2).
        open System
        type ConsolePrompt(message : string) =
            do
                if String.IsNullOrWhiteSpace(message) then
                    raise <| ArgumentException("Null or empty", "message")
            let message = message.Trim()
            member this.GetValue() =
                printfn "%s:" message
                let input = Console.ReadLine()
                if not (String.IsNullOrWhiteSpace(input)) then
                    input
                else
                    Console.Beep()
                    this.GetValue()
        let demo() =
            // System.ArgumentException: Null or empty
            // Parameter name: message
            let first = ConsolePrompt(null)
            printfn "Hello %s" (first.GetValue())
Listing 8-2

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

If you want to publish constructor values (or values derived from them) as members, you can do so as in Listing 8-3.
        open System
        type ConsolePrompt(message : string) =
            do
                if String.IsNullOrWhiteSpace(message) then
                    raise <| ArgumentException("Null or empty", "message")
            let message = message.Trim()
            member __.Message =
                message
            member this.GetValue() =
                printfn "%s:" message
                let input = Console.ReadLine()
                if not (String.IsNullOrWhiteSpace(input)) then
                    input
                else
                    Console.Beep()
                    this.GetValue()
        let first = ConsolePrompt("First name")
        // First name
        printfn "%s" first.Message
Listing 8-3

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

If you want simple properties that can be retrieved and set, but without any logic to compute the values or validate or process input, you can use member val syntax with default getters and setters. For example, in Listing 8-4 I’ve amended ConsolePrompt so that you can control whether there is a “beep” when the user enters invalid input (for example, an empty string). There’s a settable BeepOnError property that GetValue consults to decide whether to beep.
        open System
        type ConsolePrompt(message : string) =
            do
                if String.IsNullOrWhiteSpace(message) then
                    raise <| ArgumentException("Null or empty", "message")
            let message = message.Trim()
            member val BeepOnError = true
                with get, set
            member __.Message =
                message
            member this.GetValue() =
                printfn "%s:" message
                let input = Console.ReadLine()
                if not (String.IsNullOrWhiteSpace(input)) then
                    input
                else
                    if this.BeepOnError then
                        Console.Beep()
                    this.GetValue()
        let demo() =
            let first = ConsolePrompt("First name")
            first.BeepOnError <- false
            let name = first.GetValue()
            // No beep on invalid input!
            printfn "%s" name
Listing 8-4

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

You might disagree with my assertion above that BeepOnError isn’t an important enough property to have in a constructor parameter. After all, by making it a member val we have cracked open the door to mutability, something we could have avoided by making beepOnError a constructor parameter. Luckily you can dodge the whole issue by declaring additional constructors (Listing 8-5).
        open System
        type ConsolePrompt(message : string, beepOnError : bool) =
            do
                if String.IsNullOrWhiteSpace(message) then
                    raise <| ArgumentException("Null or empty", "message")
            let message = message.Trim()
            new (message : string) =
                ConsolePrompt(message, true)
            member this.GetValue() =
                printfn "%s:" message
                let input = Console.ReadLine()
                if not (String.IsNullOrWhiteSpace(input)) then
                    input
                else
                    if beepOnError then
                        Console.Beep()
                    this.GetValue()
        let demo() =
            let first = ConsolePrompt("First name", false)
            let last = ConsolePrompt("Second name")
            // No beep on invalid input!
            let firstName = first.GetValue()
            // Beep on invalid input!
            let lastName = last.GetValue()
            printfn "Hello %s %s" firstName lastName
Listing 8-5

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

Going back to mutable properties: sometimes auto-implemented properties aren’t sufficient for your needs. What do you do if, for instance, you want to validate the value that is being set, or calculate a value on demand? For these kinds of operations, you can use explicit getters and setters. Let’s extend the ConsolePrompt class so that you can set the foreground and background colors of the prompt, and let’s also make a rule that the foreground and background colors can’t be the same (Listing 8-6).
open System
type ConsolePrompt(message : string) =
    let mutable foreground = ConsoleColor.White
    let mutable background = ConsoleColor.Black
    member __.ColorScheme
        with get() =
            foreground, background
        and set(fg, bg) =
            if fg = bg then
                raise <| ArgumentException(
                            "Foreground, background can't be same")
            foreground <- fg
            background <- bg
    member this.GetValue() =
        Console.ForegroundColor <- foreground
        Console.BackgroundColor <- background
        printfn "%s:" message
        Console.ResetColor()
        let input = Console.ReadLine()
        if not (String.IsNullOrWhiteSpace(input)) then
            input
        else
            this.GetValue()
let demo() =
    let first = ConsolePrompt("First name")
    // System.ArgumentException: Foreground, background can't be same
    first.ColorScheme <- (ConsoleColor.Red, ConsoleColor.Red)
Listing 8-6

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.

In contrast to BeepOnError in Listing 8-4, the ColorScheme property in Listing 8-6 has the following extras:
  • 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 mutable state doesn’t have to be accessible from outside. You can declare and initialize a mutable value in the class constructor, and update it anywhere in the body. Listing 8-7 shows a version of ConsolePrompt that limits the number of attempts to enter a valid input.
        open System
        type ConsolePrompt(message : string, maxAttempts : int) =
            let mutable attempts = 1
            member this.GetValue() =
                printfn "%s:" message
                let input = Console.ReadLine()
                if not (String.IsNullOrWhiteSpace(input)) then
                    input
                elif attempts < maxAttempts then
                    attempts <- attempts + 1
                    this.GetValue()
                else
                    raise <| Exception("Max attempts exceeded")
        let demo() =
            let first = ConsolePrompt("First name", 2)
            let name = first.GetValue()
            // Exception if you try more than twice:
            printfn "%s" name
Listing 8-7

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

ConsolePrompt would be much more useful if it could produce typed results: a string for a name, an integer for an age, and so forth. The first step to doing this is to make the class generic, by adding a type parameter after the name in the declaration, thus:
type ConsolePrompt<'T>(message : string...

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.

A great way to do all this is to require a conversion function as one of the constructor parameters. We pretty much defined the necessary function signature in the previous paragraph: we need to go from a string to a value of some type - and we might fail. So the conversion signature needs to be string -> 'T option. Putting all that together leads to a type signature like this:
type ConsolePrompt<'T>
   (message : string, maxAttempts : int, tryConvert : string -> 'T option) =

(I’ve kept the maxAttempts parameter from the previous section as it’s relevant to how we’ll need to use the converter.)

Now we’re ready to update GetValue() to use the converter. In rough pseudocode the logic needs to be:
  • 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.

Listing 8-8 contains an initial implementation of that logic.
        open System
        type ConsolePrompt<'T>
            (message : string, maxAttempts : int,
             tryConvert : string -> 'T option) =
            let mutable attempts = 1
            member this.GetValue() =
                printfn "%s:" message
                let input =  Console.ReadLine()
                match input |> tryConvert with
                | Some v -> v
                | None ->
                    if attempts < maxAttempts then
                        attempts <- attempts + 1
                        this.GetValue()
                    else
                        raise <| Exception("Max attempts exceeded")
Listing 8-8

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.

Now we need some code to exercise our nice generic class. Listing 8-9 shows us using it to get a name as a string, and an age as an integer. We define suitable conversion functions and provide them to ConsolePrompt instances. Note incidentally how the string validation logic we had earlier (checking the string isn’t null or whitespace) has moved to the string conversion function.
        let tryConvertString (s : string) =
            if String.IsNullOrWhiteSpace(s) then
                None
            else
                Some s
        let tryConvertInt (s : string) =
            match Int32.TryParse(s) with
            | true, x -> Some x
            | false, _ -> None
        let demo() =
            let namePrompt = ConsolePrompt("Name", 2, tryConvertString)
            let agePrompt = ConsolePrompt("Age", 2, tryConvertInt)
            let name = namePrompt.GetValue()
            let age = agePrompt.GetValue()
            printfn "Name: %s Age: %i" name age
Listing 8-9

Using the generic ConsolePrompt class

This works absolutely fine, and I would be strongly tempted to leave the code at that. But you might remember from Chapter 3 that there is often a better alternative to explicitly pattern matching on option types. In Listing 8-10 I’ve restated the body of GetValue() to use Option.defaultWith , thus avoiding explicit pattern matching.
            member this.GetValue() =
                printfn "%s:" message
                Console.ReadLine()
                |> tryConvert
                |> Option.defaultWith (fun () ->
                    if attempts < maxAttempts then
                        attempts <- attempts + 1
                        this.GetValue()
                    else
                        raise <| Exception("Max attempts exceeded"))
Listing 8-10

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

One problem for the reader of any code is working out, at the call site, what the various arguments going into a constructor or method actually mean. This is particularly acute for Boolean flags, where a value of true or false gives no clue, at the call site, to what the value might do. It’s time to demonstrate our commitment to the principle of semantic focus by looking at some alternative construction styles (Listing 8-11).
            // Requires the version of ConsolePrompt from Listing 8-4.
            // No argument names:
            let namePrompt1 =
                ConsolePrompt("Name", 2, tryConvertString)
            // With argument names:
            let namePrompt2 =
                ConsolePrompt(
                    message = "Name",
                    maxAttempts = 2,
                    tryConvert = tryConvertString)
            // No argument names, but with object initialization:
            let namePrompt3 =
                ConsolePrompt(
                    "Name",
                    2,
                    tryConvertString,
                    BeepOnError = false)
            // With argument names and object initialization:
            let namePrompt4 =
                ConsolePrompt(
                    message = "Name",
                    maxAttempts = 2,
                    tryConvert = tryConvertString,
                    BeepOnError = false)
Listing 8-11

Alternative construction styles

In Listing 8-11, I show four variations on constructor calling. (I’ve separated the arguments onto separate lines because of page width limitations; you don’t have to do this unless you want to.)
  • 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

We’ve already learned how to provide simple properties in a class by providing default or explicit getters and setters. That’s fine if the properties are single values, but what if you want a class to provide a collection property: one that you can access with the syntax property.[index]? For example, let’s implement a ring buffer: a structure that contains a collection of length n. When we access elements beyond the last, we circle back to element number (index modulus length). For instance, in Figure 8-1, element [8] is actually the same item as element [0].
../images/462726_1_En_8_Chapter/462726_1_En_8_Fig1_HTML.png
Figure 8-1

A Ring Buffer

Listing 8-12 shows a simple ring buffer implementation that is initialized with values from a sequence.
        type RingBuffer<'T>(items : 'T seq) =
            let _items = items |> Array.ofSeq
            let length = _items.Length
            member __.Item i =
                _items.[i % length]
        let demo() =
            let fruits = RingBuffer(["Apple"; "Orange"; "Pear"])
            // Apple Orange Pear Apple Orange Pear Apple Orange
            for i in 0..7 do
                printfn "%s" fruits.[i]
            // Invalid assignment
            // fruits.[4] <- "Grape"
Listing 8-12

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.

If you want indexed properties to be settable, you need to use a slightly different syntax, one with an explicit getter and setter (Listing 8-13).
        type RingBuffer<'T>(items : 'T seq) =
            let _items = items |> Array.ofSeq
            let length = _items.Length
            member __.Item
                with get(i) =
                    _items.[i % length]
                and set i value =
                    _items.[i % length] <- value
        let demo() =
            let fruits = RingBuffer(["Apple"; "Orange"; "Pear"])
            fruits.[4] <- "Grape"
            // Apple Grape Pear Apple Grape Pear Apple Grape
            for i in 0..7 do
                printfn "%s" fruits.[i]
Listing 8-13

Settable indexed properties

You can also have multidimensional indexed properties. Listing 8-14 implements a (slightly mind-bending) two-dimensional ring buffer. Maybe this could be used to represent a 2D gaming environment with wrap-around when a player went beyond the finite bounds.
        type RingBuffer2D<'T>(items : 'T[,]) =
            let leni = items.GetLength(0)
            let lenj = items.GetLength(1)
            let _items = Array2D.copy items
            member __.Item
                with get(i, j) =
                    _items.[i % leni, j % lenj]
                and set (i, j) value =
                    _items.[i % leni, j % lenj] <- value
        let demo() =
            let numbers = Array2D.init 4 5 (fun x y -> x * y)
            let numberRing = RingBuffer2D(numbers)
            // 0 0 -> 0
            // 0 1 -> 0
            // ...
            // 1 1 -> 1
            // 1 2 -> 2
            // ..
            // 9 8 -> 3
            // 9 9 -> 4
            for i in 0..9 do
                for j in 0..9 do
                    printfn "%i %i -> %A" i j (numberRing.[i,j])
Listing 8-14

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.

Let’s imagine we want to have an interface that defines a simple media player. The player needs to know how to open, play, stop playing, and eject media items. We want to specify these behaviors in abstract terms, without worrying about whether a player implementation plays audio, video, or something else (smell?); or thinking about how it does so. Listing 8-15 shows an interface definition that meets these requirements.
        type MediaId = string
        type TimeStamp = int
        type Status =
            | Empty
            | Playing of MediaId * TimeStamp
            | Stopped of MediaId
        type IMediaPlayer =
            abstract member Open : MediaId -> unit
            abstract member Play : unit -> unit
            abstract member Stop : unit -> unit
            abstract member Eject : unit -> unit
            abstract member Status : unit -> Status
Listing 8-15

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.

Now we have an interface definition, it’s time to start implementing the interface: in other words, writing at least one class that provides actual code to execute for each of the abstract members in the interface. To implement an interface, start by defining a class in the usual way, providing a name and a constructor body (Listing 8-16).
        type DummyPlayer() =
            let mutable status = Empty
            interface IMediaPlayer with
                member __.Open(mediaId : MediaId) =
                    printfn "Opening '%s'" mediaId
                    status <- Stopped mediaId
                member __.Play() =
                    match status with
                    | Empty
                    | Playing(_, _) -> ()
                    | Stopped(mediaId) ->
                        printfn "Playing '%s'" mediaId
                        status <- Playing(mediaId, 0)
                member __.Stop() =
                    match status with
                    | Empty
                    | Stopped(_) -> ()
                    | Playing(mediaId, _) ->
                        printfn "Stopping '%s'" mediaId
                        status <- Stopped(mediaId)
                member __.Eject() =
                    match status with
                    | Empty -> ()
                    | Stopped(_)
                    | Playing(_, _) ->
                        printfn "Ejecting"
                        status <- Empty
                member __.Status() =
                    status
Listing 8-16

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.

Now we have a class that implements the interface, we can use it in code. There is one minor complication, which always trips people up, because it is a different behavior from that in C#. To access any interface members, you must cast the concrete class instance to the interface type, which you do with the :> (upcast) operator (Listing 8-17).
        let demo() =
            let player = new DummyPlayer() :> IMediaPlayer
            // "Opening 'Dreamer'"
            player.Open("Dreamer")
            // "Playing 'Dreamer'"
            player.Play()
            // "Ejecting"
            player.Eject()
            // "Empty"
            player.Status() |> printfn "%A"
Listing 8-17

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!”

In Listing 8-17 I cast to the interface as soon as I’ve constructed the class instance. This is appropriate here because I am only accessing members that were part of the interface. You can’t get away with this if you need to access members that aren’t part of the interface – either direct members of the class itself, or members from some other interface that the class also implements. In those cases, you’ll need to cast the interface just before you access the relevant members (Listing 8-18).
        type MediaId = string
        type TimeStamp = int
        type Status =
            ...code as Listing 8-17...
        type IMediaPlayer =
            ...code as Listing 8-17...
        open System
        open System.IO
        type DummyPlayer() =
            let uniqueId = Guid.NewGuid()
            let mutable status = Empty
            let stream = new MemoryStream()
            member __.UniqueId =
                uniqueId
            interface IMediaPlayer with
                member __.Open(mediaId : MediaId) =
                    printfn "Opening '%s'" mediaId
                    status <- Stopped mediaId
                member __.Play() =
                    match status with
                    | Empty
                    | Playing(_, _) -> ()
                    | Stopped(mediaId) ->
                        printfn "Playing '%s'" mediaId
                        status <- Playing(mediaId, 0)
                member __.Stop() =
                    match status with
                    | Empty
                    | Stopped(_) -> ()
                    | Playing(mediaId, _) ->
                        printfn "Stopping '%s'" mediaId
                        status <- Stopped(mediaId)
                member __.Eject() =
                    match status with
                    | Empty -> ()
                    | Stopped(_)
                    | Playing(_, _) ->
                        printfn "Ejecting"
                        status <- Empty
                member __.Status() =
                    status
            interface IDisposable with
                member __.Dispose() =
                    stream.Dispose()
        let demo() =
            let player = new DummyPlayer()
            (player :> IMediaPlayer).Open("Dreamer")
            // 95cf8c51-ee29-4c99-b714-adbe1647b62c
            printfn "%A" player.UniqueId
            (player :> IDisposable).Dispose()
Listing 8-18

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.

Let’s say you are testing some class that takes a logger as one of its constructor arguments, and uses that logger throughout its implementation. You don’t want the overhead of creating a real logger instance; you just want a simple dummy logger that writes to the console or even does nothing. Listing 8-19 shows how to do that for the MediaPlayer example, without creating any new types.
        type ILogger =
            abstract member Info : string -> unit
            abstract member Error : string -> unit
        type MediaId = string
        type TimeStamp = int
        type Status =
            ...code as Listing 8-18...
        type IMediaPlayer =
            ...code as Listing 8-18...
        type LoggingPlayer(logger : ILogger) =
            let mutable status = Empty
            interface IMediaPlayer with
                member __.Open(mediaId : MediaId) =
                    logger.Info(sprintf "Opening '%s'" mediaId)
                    status <- Stopped mediaId
                member __.Play() =
                    match status with
                    | Empty ->
                        logger.Error("Nothing to play")
                    | Playing(_, _) ->
                        logger.Error("Already playing")
                    | Stopped(mediaId) ->
                        logger.Info(sprintf "Playing '%s'" mediaId)
                        status <- Playing(mediaId, 0)
                member __.Stop() =
                    match status with
                    | Empty
                    | Stopped(_) ->
                        logger.Error("Not playing")
                    | Playing(mediaId, _) ->
                        logger.Info(sprintf "Playing '%s'" mediaId)
                        status <- Stopped(mediaId)
                member __.Eject() =
                    match status with
                    | Empty ->
                        logger.Error("Nothing to eject")
                    | Stopped(_)
                    | Playing(_, _) ->
                        logger.Info("Ejecting")
                        status <- Empty
                member __.Status() =
                    status
        let demo() =
            let logger = {
                new ILogger with
                    member __.Info(msg) = printfn "%s" msg
                    member __.Error(msg) = printfn "%s" msg }
            let player = new LoggingPlayer(logger) :> IMediaPlayer
            // "Nothing to eject"
            player.Eject()
            // "Opening 'Dreamer'"
            player.Open("Dreamer")
            // "Ejecting"
            player.Eject()
Listing 8-19

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

An abstract class is, broadly speaking, a class that allows at least some of its members to be implemented by derived classes. The concept in F# is not precisely the same as it is in C#, so if you are going to use abstract classes, it’s good to be clear about F#’s interpretation of “abstract”:
  • 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

Listing 8-20 shows a simple class hierarchy with one abstract class and one derived class.
        [<AbstractClass>]
        type AbstractClass() =
            abstract member SaySomething : string -> string
        type ConcreteClass(name : string) =
            inherit AbstractClass()
            override __.SaySomething(whatToSay) =
                sprintf "%s says %s" name whatToSay
        let demo() =
            let cc = ConcreteClass("Concrete")
            // "Concrete says hello"
            cc.SaySomething("hello")
Listing 8-20

A simple abstract class

The abstract class’s member SaySomething is defined using the same kind of syntax as we used when defining an interface: we specify the name of the member and its signature (in this case string -> string). Importantly, we use the [<AbstractClass>] attribute because this truly is an abstract class: it has at least one abstract member that doesn’t have a default definition. The error message you get if you omit the [<AbstractClass>] attribute is a little confusing:
error FS0365: No implementation was given for 'abstract member AbstractClass.SaySomething : string -> string'

So don’t forget the attribute!

Default Member Implementations

Sometimes we want to provide a default implementation for an abstract member: one that will be used if a derived class doesn’t bother to override that member. Default implementations are defined separately from the abstract definition. You use the same syntax as for ordinary members, except by using the keyword default instead of member (Listing 8-21).
        type ParentClass() =
            abstract member SaySomething : string -> string
            default __.SaySomething(whatToSay) =
                sprintf "Parent says %s" whatToSay
        type ConcreteClass1(name : string) =
            inherit ParentClass()
        type ConcreteClass2(name : string) =
            inherit ParentClass()
            override __.SaySomething(whatToSay) =
                sprintf "%s says %s" name whatToSay
        let demo() =
            let cc1 = ConcreteClass1("Concrete 1")
            let cc2 = ConcreteClass2("Concrete 2")
            // "Parent says hello"
            printfn "%s" (cc1.SaySomething("hello"))
            // "Concrete 2 says hello"
            printfn "%s" (cc2.SaySomething("hello"))
Listing 8-21

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

Back in Chapter 7 we dodged the issue of latitude/longitude equality by storing positions as F# records, which by default implement structural (content) equality. Now it’s time to revisit the issue in the world of classes, where reference equality is the default. Consider the code in Listing 8-22, and note how two instances of LatLon, landsEnd, and landsEnd2, are considered unequal even though they refer to the same geographical position.
        type LatLon(latitude : float, longitude : float) =
            member __.Latitude = latitude
            member __.Longitude = longitude
        let landsEnd = LatLon(50.07, -5.72)
        let johnOGroats = LatLon(58.64, -3.07)
        let landsEnd2 = LatLon(50.07, -5.72)
        // false
        printfn "%b" (landsEnd = johnOGroats)
        // false
        printfn "%b" (landsEnd = landsEnd2)
Listing 8-22

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.

Let’s start by simply making our class implement IEquatable , which is just a matter of overriding the Equals method (Listing 8-23).
        open System
        type LatLon(latitude : float, longitude : float) =
            member __.Latitude = latitude
            member __.Longitude = longitude
            interface IEquatable<LatLon> with
                member this.Equals(that : LatLon) =
                    this.Latitude = that.Latitude
                    && this.Longitude = that.Longitude
        let demo()  =
            let landsEnd = LatLon(50.07, -5.72)
            let johnOGroats = LatLon(58.64, -3.07)
            let landsEnd2 = LatLon(50.07, -5.72)
            // false
            printfn "%b" (landsEnd = johnOGroats)
            // false
            printfn "%b" (landsEnd = landsEnd2)
Listing 8-23

Just implementing IEquatable isn’t enough

This compiles absolutely fine, and you might be forgiven for relaxing at that point. Except that it doesn’t work! In the last line of Listing 8-23, landsEnd = landsEnd2 still returns false. Confusingly, in the case of IEquatable, it isn’t enough just to implement the interface. For a start, you must also override the Equals() method of System.Object . (All classes are derived ultimately from System.Object, and for low-level operations like equality you do sometimes have to override its methods.) Listing 8-24 shows us doing everything needed to make basic equality work.
        open System
        [<AllowNullLiteral>]
        type LatLon(latitude : float, longitude : float) =
            let eq (that : LatLon) =
                if isNull that then
                    false
                else
                    latitude = that.Latitude
                    && longitude = that.Longitude
            member __.Latitude = latitude
            member __.Longitude = longitude
            override this.GetHashCode() =
                hash (this.Latitude, this.Longitude)
            override __.Equals(thatObj) =
                match thatObj with
                | :? LatLon as that ->
                    eq that
                | _ ->
                    false
            interface IEquatable<LatLon> with
                member __.Equals(that : LatLon) =
                    eq that
Listing 8-24

Overriding Object.Equals

We’ve made a number of changes between Listings 8-23 and 8-24, and to make equality work correctly, you’ll often need to do all of these things:
  • 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.

The GetHashCode() aspect of these changes needs a little more explanation. If you don’t override GetHashCode(), equality for the class will work correctly, but you’ll get a compiler warning:
Warning FS0346: The struct, record or union type 'LatLon' has an explicit implementation of 'Object.Equals'. Consider implementing a matching override for 'Object.GetHashCode()'
In case you didn’t already know, GetHashCode() is a method that returns a “magic number” that has the following qualities:
  • 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.

With all these changes in place, Listing 8-25 demonstrates that equality now works correctly from F#, including directly comparing instances with the = operator and adding them to a dictionary, which requires equality.
        // Requires code from Listing 8-25
        let demo() =
            let landsEnd = LatLon(50.07, -5.72)
            let johnOGroats = LatLon(58.64, -3.07)
            let landsEnd2 = LatLon(50.07, -5.72)
            // false
            printfn "%b" (landsEnd = johnOGroats)
            // true
            printfn "%b" (landsEnd = landsEnd2)
            let places = [ landsEnd; johnOGroats; landsEnd2 ]
            let placeDict =
                places
                |> Seq.mapi (fun i place -> place, i)
                |> dict
            // 50.070000, -5.720000 -> 2
            // 58.640000, -3.070000 -> 1
            placeDict
            |> Seq.iter (fun kvp ->
                printfn "%f, %f -> %i"
                    kvp.Key.Latitude kvp.Key.Longitude kvp.Value)
Listing 8-25

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).

There is one final thing we need to do in regard to equality: ensure that the == operator works correctly from C# and VB.NET. To do this, add another override for the op_Equality method (Listing 8-26).
            // static member ( = ) : this:LatLon * that:LatLon -> bool
            static member op_Equality(this : LatLon, that : LatLon) =
                this.Equals(that)
Listing 8-26

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.

Here’s how to implement IComparable for our LatLon class (Listing 8-27).
open System
[<AllowNullLiteral>]
type LatLon(latitude : float, longitude : float) =
    ...code as Listing 8-24...
    interface IComparable with
        member this.CompareTo(thatObj) =
            match thatObj with
            | :? LatLon as that ->
                compare
                    (this.Latitude, this.Longitude)
                    (that.Latitude, that.Longitude)
            | _ ->
                raise <| ArgumentException("Can't compare different types")
Listing 8-27

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.

In Listing 8-28 we prove that a list of LatLon instances from Listing 8-27 can be put into a Set, and that duplicates by geographical position are eliminated in the process.
let demo() =
    let landsEnd = LatLon(50.07, -5.72)
    let johnOGroats = LatLon(58.64, -3.07)
    let landsEnd2 = LatLon(50.07, -5.72)
    let places = [ landsEnd; johnOGroats; landsEnd2 ]
    // 50.070000, -5.720000
    // 58.640000, -3.070000
    places
    |> Set.ofList
    |> Seq.iter (fun ll -> printfn "%f, %f" ll.Latitude ll.Longitude)
Listing 8-28

Using class instances that implement IComparable

One final wrinkle: there are actually two versions of IComparable , a non-generic and a generic one. In Listing 8-27 we only implemented the non-generic one. This works, but there can be a benefit in also implementing the generic version. Some APIs will try to use both, starting with the generic version, which can improve performance. Listing 8-29 shows how to add the generic version of IComparable to the LatLon definition.
        open System
        [<AllowNullLiteral>]
        type LatLon(latitude : float, longitude : float) =
            let eq (that : LatLon) =
                if isNull that then
                    false
                else
                    latitude = that.Latitude
                    && longitude = that.Longitude
            let comp (that : LatLon) =
                compare
                    (latitude, longitude)
                    (that.Latitude, that.Longitude)
            member __.Latitude = latitude
            member __.Longitude = longitude
            static member op_Equality(this : LatLon, that : LatLon) =
                this.Equals(that)
            override this.GetHashCode() =
                hash (this.Latitude, this.Longitude)
            override __.Equals(thatObj) =
                match thatObj with
                | :? LatLon as that ->
                    eq that
                | _ ->
                    false
            interface IEquatable<LatLon> with
                member __.Equals(that : LatLon) =
                    eq that
            interface IComparable with
                member __.CompareTo(thatObj) =
                    match thatObj with
                    | :? LatLon as that ->
                        comp that
                    | _ ->
                        raise <| ArgumentException("Can't compare different types")
            interface IComparable<LatLon> with
                member __.CompareTo(that) =
                   comp that
Listing 8-29

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

Here are the ideas I’d like you to take away from this chapter:
  • 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!

Object Orientation has its own internal logic that, when followed, doesn’t always lead to the simplest solution. Therefore, you need to be keenly aware of the costs (and, to be fair, benefits) of even starting down this path for any particular piece of design. The costs are, broadly speaking:
  • 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.

At the same time, you shouldn’t discount the benefits of an OO approach:
  • .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

Override the ToString() method of GrayScale so that it produces output like this, where the number is the Level value:
Greyscale(140)

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

This can be done in three lines of code. Note the casting between byte and int and back again. This is done so that there is no overflow during the addition, but the Level property is still a byte.
        type GreyScale(r : byte, g : byte, b : byte) =
            member __.Level =
                (int r + int g + int b) / 3 |> byte
        let demo() =
            // 255
            GreyScale(255uy, 255uy, 255uy).Level |> printfn "%i"

Exercise 8-2 – Secondary Constructors

Add a secondary constructor using the new keyword, and pass the color values individually through to the main constructor.
        open System.Drawing
        type GreyScale(r : byte, g : byte, b : byte) =
            new (color : Color) =
                GreyScale(color.R, color.G, color.B)
            member __.Level =
                (int r + int g + int b) / 3 |> byte
        let demo() =
            // 83
            GreyScale(Color.Brown).Level |> printfn "%i"

Exercise 8-3 – Overrides

Add a straightforward override and use sprintf to produce the formatted output.
        open System.Drawing
        type GreyScale(r : byte, g : byte, b : byte) =
            new (color : Color) =
                GreyScale(color.R, color.G, color.B)
            member __.Level =
                (int r + int g + int b) / 3 |> byte
            override this.ToString() =
                sprintf "Greyscale(%i)" this.Level
        let demo() =
            // Greyscale(140)
            GreyScale(Color.Orange) |> printfn "%A"
            // Greyscale(255)
            GreyScale(255uy, 255uy, 255uy) |> printfn "%A"

Exercise 8-4 – Equality

Follow the pattern shown in Listing 8-24, but since you have not added the [<AllowNullLiteral>] attribute, you shouldn’t check for null in the eq function.
        open System
        open System.Drawing
        type GreyScale(r : byte, g : byte, b : byte) =
            let level = (int r + int g + int b) / 3 |> byte
            let eq (that : GreyScale) =
                level = that.Level
            new (color : Color) =
                GreyScale(color.R, color.G, color.B)
            member __.Level =
                level
            override this.ToString() =
                sprintf "Greyscale(%i)" this.Level
            override this.GetHashCode() =
                hash level
            override __.Equals(thatObj) =
                match thatObj with
                | :? GreyScale as that ->
                    eq that
                | _ ->
                    false
            interface IEquatable<GreyScale> with
                member __.Equals(that : GreyScale) =
                    eq that
        let demo() =
            let orange1 = GreyScale(Color.Orange)
            let blue = GreyScale(Color.Blue)
            let orange2 = GreyScale(0xFFuy, 0xA5uy, 0x00uy)
            let orange3 = GreyScale(0xFFuy, 0xA5uy, 0x01uy)
            // true
            printfn "%b" (orange1 = orange2)
            // false
            printfn "%b" (orange1 = blue)
            // true
            printfn "%b" (orange1 = orange3)

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.218.28