left-icon

F# Succinctly®
by Robert Pickering

Previous
Chapter

of
A
A
A

CHAPTER 5

Object-Oriented Programming

Object-Oriented Programming


Object-oriented programming is the third major programming paradigm. There has been a tendency to try and show the functional paradigm and the object-oriented paradigm as competing, but I believe them to be complementary techniques that work well together, which I will try to demonstrate in this chapter. At its heart, object-oriented programming has a few simple ideas, sometimes referred to as the tenets of object-oriented programming: encapsulation, polymorphism, and inheritance.

Possibly the most important tenet is encapsulation—the idea that the implementations and state should be encapsulated, or hidden behind well-defined boundaries. This makes the structure of a program easier to manage. In F#, you hide things by using signatures for modules and type definitions, as well as by simply defining them locally to an expression or class construction (you’ll see examples of both in this chapter).

The second tenet, polymorphism, is the idea that you can implement abstract entities in multiple ways. You’ve met a number of simple abstract entities already, such as function types. A function type is abstract because you can implement a function with a specific type in many different ways. For example, you can implement the function type int -> int as a function that increments the given parameter, a function that decrements the parameter, or any one of millions of mathematical sequences. You can also build other abstract entities out of existing abstract components, such as the interface types defined in the .NET BCL. You can also model more sophisticated abstract entities using user-defined interface types. Interface types have the advantage that you can arrange them hierarchically; this is called interface inheritance. For example, the .NET BCL includes a hierarchical classification of collection types, available in the System.Collections and System.Collections.Generic namespaces.

In OOP, you can sometimes arrange implementation fragments hierarchically. This is called implementation inheritance, and it tends to be less important in F# programming because of the flexibility that functional programming provides for defining and sharing implementation fragments. However, it is significant for domains such as graphical user interface (GUI) programming.

While the tenets of object-oriented programming are important, object-oriented programming has also become synonymous with organizing your code around the values of the system nouns and then providing operations on those values as members, functions, or methods that operate on this value. This is often as simple as taking a function written in the style where the function is applied to a value (such as String.length s) and rewriting it using the dot notation (such as s.Length). This simple act can often make your code a good deal clearer. You’ll see in this chapter how F# allows you to attach members to any of its types, not just its classes, enabling you to organize all your code in an object-oriented style if you wish.

F# provides a rich object-oriented programming model that allows you to create classes, interfaces, and objects that behave similarly to those created by C# and VB.NET. Perhaps more importantly, the classes you create in F# are indistinguishable from those that are created in other languages when packaged in a library and viewed by a user of that library. However, object-oriented programming is more than simply defining objects, as you’ll see when you start looking at how you can program in an object-oriented style using F# native types.

F# Types with Members

It is possible to add functions to both F#’s record and union types. You can call a function added to a record or union type using dot notation, just as you can a member of a class from a library not written in F#. It also proves useful when you want to expose types you define in F# to other .NET languages. Many programmers prefer to see function calls made on an instance value, and this technique provides a nice way of doing this for all F# types.

The syntax for defining an F# record or union type with members is the same as the syntax you learned in Chapter 4, except here it includes member definitions that always come at the end, after the with keyword. The definition of the members themselves starts with the keyword member, followed by an identifier that represents the parameter of the type the member is being attached to, followed by a dot, the function name, and then any other parameters the function takes. After this comes an equal sign followed by the function definition, which can be any F# expression.

The following example defines a record type, Point. It has two fields, Left and Top; and a member function, Swap. The Swap function is a simple function that creates a new point with the values of Left and Top swapped. Note how to use the x parameter, given before the function name Swap, within the function definition to access the record’s other members:

// A point type.

type Point =

    { Top: int;

      Left: int }

    with

        // The swap member creates a new point

        // with the left/top coords reversed.

        member x.Swap() =

            { Top = x.Left;

              Left = x.Top }

// Create a new point.

let myPoint =

    { Top = 3;

      Left = 7 }

    

let main() =

    // Print the inital point.

    printfn "%A" myPoint

    // Create a new point with the coordinates swapped.

    let nextPoint = myPoint.Swap()

    // Print the new point.

    printfn "%A" nextPoint

// Start the app.

do main()

When you compile and execute this example, you get the following results:

{Top = 3;

 Left = 7;}

{Top = 7;

 Left = 3;}

You might have noticed the x parameter in the definition of the function Swap:

member x.Swap() =

    { Top = x.Left;

      Left = x.Top }

This is the parameter that represents the object on which the function is being called. Now look at the case where you call a function on a value:

let nextPoint = myPoint.Swap()

The value you call the function on is passed to the function as an argument. This is logical when you consider that the function needs to be able to access the fields and methods of the value on which you call it. Some OO languages use a specific keyword for this, such as this or Me, but F# lets you choose the name of this parameter by specifying a name for it after the keyword member—x, in this case.

Union types can have member functions, too. You define them in the same way that you define record types. The next example shows a union type, DrinkAmount, which has a function added to it:

// A type representing the amount of a specific drink.

type DrinkAmount =

    | Coffee of int

    | Tea of int

    | Water of int

    with

        // Get a string representation of the value.

        override x.ToString() =

            match x with

            | Coffee x -> Printf.sprintf "Coffee: %i" x

            | Tea x -> Printf.sprintf "Tea: %i" x

            | Water x -> Printf.sprintf "Water: %i" x

// Create a new instance of DrinkAmount.

let t = Tea 2

// Print out the string.

printfn "%s" (t.ToString())

When you compile and execute this code, you get the following results:

Tea: 2

Note how this uses the keyword override in place of the keyword member. This has the effect of replacing, or overriding, an existing function of the base type. This is not a very common practice with function members associated with F# types because only four methods are available to be overridden: ToString, Equals, GetHashCode, and Finalize. Every .NET type inherits these from System.Object. Because of the way some of these methods interact with the CLR, the only one I recommend overriding is ToString. Only four methods are available for overriding because record and union types can’t act as base or derived classes, so you cannot inherit methods to override (except from System.Object).

Defining Classes

You have already seen quite a few examples of using classes defined in the .NET BCL library; next, you’ll learn how to define your own classes. In object-oriented programming, a class should model some concept used within the program or library you are creating. For example, the String class models a collection of characters, and the Process class models an operating-system process.

A class is a type, so a class definition starts with the type keyword, followed by the name of the class and the parameters of the class’ constructor between parentheses. Next comes an equal sign, followed by a class’ member definitions. The most basic member of a class is called a method, which is a function that has access to the parameters of the class.

The next example shows a class that represents a user. The user class’ constructor takes two parameters: the user's name and a hash of the user’s password. Your class provides two member methods: Authenticate, which checks whether the user’s password is valid; and LogonMessage, which gets a user-specific logon message:

open System.Web.Security

// Give shorter name to password hashing method.

let hash = FormsAuthentication.HashPasswordForStoringInConfigFile

// A class that represents a user.

// Its constructor takes two parameters: the user's

// name and a hash of his or her password.

type User(name, passwordHash) =

    // Hashes the user's password and checks it against

    // the known hash.

    member x.Authenticate(password) =

        let hashResult = hash (password, "sha1")

        passwordHash = hashResult

    // Gets the user's logon message.

    member x.LogonMessage() =

        Printf.sprintf "Hello, %s" name

                

// Create a new instance of our user class.

let user = User("Robert", "AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")

       

let main() =

    // Authenticate user and print appropriate message.

    if user.Authenticate("badpass") then

        printfn "%s" (user.LogonMessage())

    else

        printfn "Logon failed"

do main()

The second half of the example demonstrates how to use the class. It behaves exactly like other classes you’ve seen from the .NET BCL. You can create a new instance of User using the new keyword, and then call its member methods.

It’s often useful to define values that are internal to your classes. Perhaps you need to pre-calculate a value that you share between several member methods, or maybe you need to retrieve some data for the object from an external data source. To enable this, objects can have let bindings that are internal to the object, but shared between all members of the object. You place the let bindings at the beginning of the class definition, after the equal sign, but before the first member definition. These let bindings form an implicit construct that executes when the object is constructed; if the let bindings have any side effects, these too will occur when the object is constructed. If you need to call a function that has the unit type, such as when logging the object's construction, you must prefix the function call with the do keyword.

The next example demonstrates private let bindings by taking your User class and modifying it slightly. Now the class constructor takes a firstName and lastName, which you use in the let binding to calculate the user’s fullName. To see what happens when you call a function with a side effect, you can print the user’s fullName to the console:

// A class that represents a user.

// Its constructor takes three parameters: the user's

// first name, last name, and a hash of his or her password.

type User(firstName, lastName, passwordHash) =

    // Calculate the user's full name and store for later use

    let fullName = Printf.sprintf "%s %s" firstName lastName

    // Print the user's full name as object is being constructed.

    do printfn "User: %s" fullName

   

    // Hashes the user's password and checks it against

    // the known hash.

    member x.Authenticate(password) =

        let hashResult = hash (password, "sha1")

        passwordHash = hashResult

    // Retrieves the user's full name.

    member x.GetFullname() = fullName

Notice how the members also have access to the class’ let bindings, and how the member GetFullName returns the pre-calculated fullName value.

It’s common to need to be able to change values within classes. For example, you might need to provide a ChangePassword method to reset the user’s password in the User class. F# gives you two approaches to accomplish this. You can make the object immutable—in this case, you copy the object’s parameters, changing the appropriate value as you go. This method is generally considered to fit better with functional-style programming, but it can be a little inconvenient if the object has a lot of parameters or is expensive to create. For example, doing this might be computationally expensive, or it might require lots of I/O to construct it. The following example illustrates this approach. Notice how in the ChangePassword method you call the hash function on the password parameter, passing this to the User object’s constructor along with the user’s name:

// A class that represents a user.

// Its constructor takes two parameters: the user's

// name and a hash of his or her password.

type User(name, passwordHash) =

    // Hashes the user's password and checks it against

    // the known hash.

    member x.Authenticate(password) =

        let hashResult = hash (password, "sha1")

        passwordHash = hashResult

    // Gets the user's logon message.

    member x.LogonMessage() =

        Printf.sprintf "Hello, %s" name

    // Creates a copy of the user with the password changed.

    member x.ChangePassword(password) =

        new User(name, hash password)

The alternative to an immutable object is to make the value you want to change mutable. You do this by binding it to a mutable let binding. You can see this in the next example, where you bind the class’s parameter passwordHash to a mutable let binding of the same name:

// A class that represents a user.

// Its constructor takes two parameters: the user's

// name and a hash of his or her password.

type User(name, passwordHash) =

    // Store the password hash in a mutable let

    // binding so it can be changed later.

    let mutable passwordHash = passwordHash

    // Hashes the user's password and checks it against

    // the known hash.

    member x.Authenticate(password) =

        let hashResult = hash (password, "sha1")

        passwordHash = hashResult

    // Gets the user's logon message.

    member x.LogonMessage() =

        Printf.sprintf "Hello, %s" name

    // Changes the user's password.

    member x.ChangePassword(password) =

        passwordHash <- hash password

This means you are free to update the passwordHash to a let binding, as you do in the ChangePassword method.

Defining Interfaces

Interfaces can contain only abstract methods and properties, or members that you declare using the keyword abstract. Interfaces define a contract for all classes that implement them, exposing those components that clients can use while insulating clients from their actual implementation. A class can inherit from only one base class, but it can implement any number of interfaces. Because any class implementing an interface can be treated as being of the interface type, interfaces provide similar benefits to multiple-class inheritance while avoiding the complexity of that approach.

You define interfaces by defining a type that has no constructor and where all the members are abstract. The following example defines an interface that declares two methods: Authenticate and LogonMessage. Notice how the interface name starts with a capital I. This is a naming convention that is strictly followed throughout the .NET BCL and you should follow it in your code too. It will help other programmers distinguish between classes and interfaces when reading your code:

// An interface "IUser".

type IUser =

    // Hashes the user's password and checks it against

    // the known hash.

    abstract Authenticate: evidence: string -> bool

    // Gets the user's logon message.

    abstract LogonMessage: unit -> string

let logon (user: IUser) =

    // Authenticate user and print appropriate message.

    if user.Authenticate("badpass") then

        printfn "%s" (user.LogonMessage())

    else

        printfn "Logon failed"

The second half of the example illustrates the advantages of interfaces. You can define a function that uses the interface without knowing the implementation details. You define a logon function that takes an IUser parameter and uses it to perform a logon. This function will then work with any implementations of IUser. This is extremely useful in many situations; for example, it enables you to write one set of client code that you can reuse with several different implementations of the interface.

Implementing Interfaces

To implement an interface, use the keyword interface, followed by the interface name, the keyword with, and then the code to implement the interface members. You prefix member definitions with the keyword member, but they are otherwise the same as the definition of any method or property. You can implement interfaces by either classes or structs; you can learn how to create classes in some detail in the following sections.

The next example defines, implements, and uses an interface. The interface is the same IUser interface you implemented in the previous section; here you implement it in a class called User:

open System.Web.Security

// Give shorter name to password hashing method.

let hash = FormsAuthentication.HashPasswordForStoringInConfigFile

// An interface "IUser".

type IUser =

    // Hashes the user's password and checks it against

    // the known hash.

    abstract Authenticate: evidence: string -> bool

    // Gets the user's logon message.

    abstract LogonMessage: unit -> string

// A class that represents a user.

// Its constructor takes two parameters: the user's

// name and a hash of his or her password

type User(name, passwordHash) =

    interface IUser with

        // Authenticate implementation.

        member x.Authenticate(password) =

            let hashResult = hash (password, "sha1")

            passwordHash = hashResult

        // LogonMessage implementation.

        member x.LogonMessage() =

            Printf.sprintf "Hello, %s" name

// Create a new instance of the user.

let user = User("Robert", "AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")

// Cast to the IUser interface.

let iuser = user :> IUser

// Get the logon message.

let logonMessage = iuser.LogonMessage()

let logon (iuser: IUser) =

    // Authenticate the user and print the appropriate message.

    if iuser.Authenticate("badpass") then

        printfn "%s" logonMessage

    else

        printfn "Logon failed"   

do logon user

Notice how in the middle of the example you see casting for the first time; you can find a more detailed explanation of casting at the end of the chapter in the Casting section. But for now here’s a quick summary of what happens: the identifier user is cast to the interface IUser via the downcast operator, :>:

// Create a new instance of the user.

let user = User("Robert", "AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")

// Cast to the IUser interface.

let iuser = user :> IUser

This is necessary because interfaces are explicitly implemented in F#. Before you can use the method GetLogonMessage, you must have an identifier that is of the type IUser and not just of a class that implements IUser. Toward the end of the example, you will work around this in a different way. The function logon takes a parameter of the IUser type:

let logon (iuser: IUser) =

When you call logon with a class that implements IUser, the class is implicitly downcast to this type.

Casting

Casting is a way of explicitly altering the static type of a value by either throwing information away, which is known as upcasting; or rediscovering it, which is known as downcasting. In F#, upcasts and downcasts have their own operators. The type hierarchy starts with obj (or System.Object) at the top, with all its descendants below it. An upcast moves a type up the hierarchy, while a downcast moves a type down the hierarchy.

Upcasts change a value’s static type to one of its ancestor types. This is a safe operation. The compiler can always tell whether an upcast will work because it always knows all the ancestors of a type, so it’s able to use static analysis to determine whether an upcast will be successful. An upcast is represented by a colon, followed by the greater-than sign (:>). The following code shows you how to use an upcast to convert a string to an obj:

let myObject = ("This is a string" :> obj)

Generally, you must use upcasts when defining collections that contain disparate types. If you don’t use an upcast, the compiler will infer that the collection has the type of the first element and give a compile error if elements of other types are placed in the collection. The next example demonstrates how to create an array of controls, a common task when working with Windows Forms. Notice that you upcast all the individual controls to their common base class, Control:

open System.Windows.Forms

let myControls =

    [| (new Button() :> Control);

       (new TextBox() :> Control);

       (new Label() :> Control) |]

An upcast also has the effect of automatically boxing any value type. Value types are held in memory on the program stack, rather than on the managed heap. Boxing means that the value is pushed onto the managed heap, so it can be passed around by reference. The following example demonstrates how to box a value:

let boxedInt = (1 :> obj)

A downcast changes a value’s static type to one of its descendant types; thus, it recovers information hidden by an upcast. Downcasting is dangerous because the compiler doesn’t have any way to determine statically whether an instance of a type is compatible with one of its derived types. This means you can get it wrong, and this will cause an invalid cast exception (System.InvalidCastException) to be issued at run time. Due to the inherent danger of downcasting, many developers prefer to replace it with pattern matching over .NET types. Nevertheless, a downcast can be useful in some places, so a downcast operator—composed of a colon, question mark, and greater-than sign (:?>)—is available. The next example shows you how to use downcasting:

open System.Windows.Forms

let moreControls =

    [| (new Button() :> Control);

       (new TextBox() :> Control) |]

let control =

    let temp = moreControls.[0]

    temp.Text <- "Click Me!"

    temp

let button =

    let temp = (control :?> Button)

    temp.DoubleClick.Add(fun e -> MessageBox.Show("Hello") |> ignore)

    temp

This example creates an array of two Windows control objects, upcasting them to their base class, Control. Next, it binds the first control to the control identifier; downcasts this to its specific type, Button; and adds a handler to its DoubleClick event—an event not available on the Control class.

Summary

You’ve now seen how to use two of the three major programming paradigms in F# and how flexible F# is for coding in any mix of styles.


Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.