Staying Objective - The Book of F#: Breaking Free with Managed Functional Programming (2014)

The Book of F#: Breaking Free with Managed Functional Programming (2014)

Chapter 4. Staying Objective

For years, object-oriented (OO) development has been the de facto standard for developing business software, particularly within the enterprise, so you’re probably familiar with many of its core principles. It should come as no surprise that as a .NET language, F# supports the full cast of constructs—including classes, structs, and interfaces—available in the other .NET languages. Despite its reputation as a niche language useful only for academic exercises or highly specialized software, F#’s general-purpose, multiparadigm nature makes it suitable for most development situations. With C# and Visual Basic already well established, though, why choose F# as an OO language?

A large part of the decision rests on F#’s terse syntax, but features like type inference, object expressions, and the ability to combine object-oriented and functional styles make a strong argument, too. Let’s face it, though: Even if you’re developing in a primarily functional manner, when you’re developing software on the .NET Framework you’re going to have to work with objects at some point; that’s just the nature of the platform.

In this chapter, you’ll learn how to create OO constructs in F# with less code, yet still build robust frameworks that can hold their own against similar frameworks built with more dedicated OO languages.

Classes

Conceptually, classes in F# are identical to classes in other OO languages in that they encapsulate related data and behavior as fields, properties, methods, and events (collectively called members) to model real-world objects or concepts. Like classes in C# and Visual Basic, F# classes are reference types that support single inheritance and multiple interface implementation, and can control access to their members. As with all user-defined data types in F#, you declare classes with the type keyword. (Rather than requiring different keywords for every data type you can create, the compiler infers the construct based on its structure.)

To illustrate, let’s take another look at the class definition introduced in the type inference discussion in Chapter 3.

type Person (id : Guid, name : string, age : int) =

member x.Id = id

member x.Name = name

member x.Age = age

There’s a lot of definition packed into this example. In just four lines, there’s a class with a primary constructor with three arguments and three implicit, read-only properties! While quite a departure from the other .NET languages, this terseness is just one of the ways that F# distinguishes itself.

Constructors

Constructors are the means by which new class instances are created and initialized. They’re really specialized functions that return fully initialized class instances. Classes in F# do not require a constructor, as shown here:

type ConstructorlessClass = class end

The empty class in this example is valid F# but, unlike in C#, if you don’t define a constructor, the compiler won’t automatically generate a default constructor (a constructor with no parameters). Since a memberless class that you can’t instantiate is pretty useless, your classes will typically have at least one constructor and one member.

NOTE

One reason you might choose to omit the constructor is that each of the type’s members is static; that is, it applies to the type rather than an individual instance. We’ll examine static members in detail a bit later in this chapter.

As with other OO languages, you create new class instances by invoking a constructor. In the case of our Person class there’s only one constructor, so the choice is clear.

let me = Person(Guid.NewGuid(), "Dave", 33)

Using the new keyword to create a new class instance is optional. By convention, you use the new keyword only when creating an instance of a class that implements the IDisposable interface.

F# constructors come in two flavors: primary constructors and additional constructors.

Primary Constructors

F# classes can have a primary constructor whose arguments are embedded within the type definition itself. The primary constructor’s body contains a series of let and do bindings that represent the class’s field definitions and initialization code.

type Person ①(name : string, dob : System.DateTime) =

② let age = (System.DateTime.Now - dob).TotalDays / 365.25

③ do printfn "Creating person: %s (Age: %f)" name age

member x.Name = name

member x.DateOfBirth = dob

member x.Age = age

In this example, the primary constructor includes the parameter list with type annotations ①, a single field definition for the calculated age ②, and a do binding ③ that prints the person’s name and age when the object is constructed. All of the primary constructor’s parameters are automatically available as fields throughout your class, so there’s no need to explicitly map them.

The compiler can frequently infer the types for each constructor parameter, so there’s often no need to include explicit type annotations. In the preceding example, a type annotation (or one on an intermediate binding with a type annotation) would still be needed for the dob parameter so the compiler can resolve the correct subtract operator overload. However, that’s more the exception than the rule, as shown in the next example, where the compiler can infer the types for both the name and age parameters as string and int, respectively.

type Person (name, age) =

do printfn "Creating person: %s (Age: %i)" name age

member x.Name = name

member x.Age = age

let me = Person ("Dave", 33)

By default, the primary constructor is public, but you can change that by including an access modifier before the parameter list. You might consider changing the primary constructor’s accessibility if you were implementing the Singleton pattern, which specifies that only a single instance of the type can exist, as shown here:

type Greeter private () =

static let _instance = lazy (Greeter())

static member Instance with get() = _instance.Force()

member x.SayHello() = printfn "hello"

Greeter.Instance.SayHello()

MORE ABOUT ACCESSIBILITY IN F#

Access modifiers limit the scope of bindings, types, and members throughout your program. F# differs from C# and Visual Basic in that it directly supports only the public, private, and internal modifiers. You can’t define protected class members in F# due in part to how they complicate the functional nature of the language. F# does still honor protected members defined in other languages, so they won’t be publicly accessible and you can still override them in derived classes without breaking the abstraction.

Additional Constructors

Constructors that you define beyond the primary constructor are called additional constructors. Additional constructors are defined with the new keyword followed by a parameter list and constructor body, as shown next. While additional constructors must always invoke the primary constructor, they may do so indirectly through another constructor, thereby allowing you to chain constructor calls.

type Person (name, age) =

do printfn "Creating person: %s (Age: %i)" name age

new (name) = Person(name, 0)

new () = Person("")

member x.Name = name

member x.Age = age

Additional constructors can contain their own let bindings and other expressions, but unlike those in the primary constructor, any such elements will be local to the constructor where they’re defined rather than exposed as fields.

Additional constructors can invoke additional code like a primary constructor, but instead of using a do binding they use the then keyword. In this example, each additional constructor includes the then keyword in order to print a message indicating which constructor is being invoked.

type Person (name, age) =

do printfn "Creating person: %s (Age: %i)" name age

new (name) = Person(name, 0)

then printfn "Creating person with default age"

new () = Person("")

then printfn "Creating person with default name and age"

member x.Name = name

member x.Age = age

Classes without a primary constructor behave a bit differently at initialization. When you use them, you must explicitly define fields with the val keyword, and any additional constructors must initialize any fields not decorated with the DefaultValue attribute, as shown here:

type Person =

val _name : string

val _age : int

new (name, age) = { _name = name; _age = age }

new (name) = Person(name, 0)

new () = Person("")

member x.Name = x._name

member x.Age = x._age

Self-Identifiers

Sometimes you’ll want to reference a class member within a constructor. By default, class members aren’t accessible because they require a recursive reference to the type, but you can enable self-referencing with the as keyword and a self-identifier like this:

type Person (name, age) as this =

do printfn "Creating person: %s (Age: %i)" this.Name this.Age

member x.Name = name

member x.Age = age

You can choose any name for your self-identifiers as long as you follow the normal rules for identifiers. You could even use a quoted identifier like the following ones if you really want to irritate your future self or anyone else who’s maintaining your code.

type Person (name, age) as ``This is a bad identifier`` =

do

printfn "Creating person: %s (Age: %i)"

``This is a bad identifier``.Name

``This is a bad identifier``.Age

member x.Name = name

member x.Age = age

It’s generally best to stick with short names. Common conventions are to use either x or this. But whatever you choose, be consistent!

WARNING

The compiler will generate a warning if you define a self-identifier but don’t use it in your constructor. The reason is that using the as keyword makes the class definition recursive, which results in additional run time validation that can negatively impact initializing types in your class hierarchy. Use self-identifiers in primary constructors only when you actually need them.

Fields

Fields define the data elements associated with an object. In the previous section, we took a brief look at both ways to create fields. In this section, we’ll examine field creation in more detail.

let Bindings

The first way to create fields is with let bindings in the primary constructor. These fields, which must be initialized in the primary constructor, are always private to the class. Although they must be initialized when they’re created, you can make the value mutable as in any let binding, as shown here:

type Person () =

let mutable name : string = ""

member x.Name

with get() = name

and set(v) = name <- v

Here, a mutable let binding is used to define the backing store for the Name property.

Explicit Fields

When you want a little more control over a field or your class doesn’t have a primary constructor, create an explicit field with the val keyword. Explicit fields don’t need to be initialized immediately, but in classes with a primary constructor you’ll need to decorate them with theDefaultValue attribute to ensure that the value is initialized to its appropriate “zero” value, like this:

type Person () =

[<DefaultValue>] val mutable n : string

member x.Name

with get() = x.n

and set(v) = x.n <- v

In this example, n is an explicit field. Because n is of type string, it’s initialized to null, as you can see here:

> let p = Person()

p.Name;;

val p : Person

val it : string = null

Explicit fields are public by default, but you can make them private by including the private access modifier in the definition like this:

type Person () =

[<DefaultValue>] val mutable private n : string

-- snip --

Properties

Like fields, properties represent data associated with an object. Unlike fields, though, properties offer more control over how that data is accessed or modified by exposing the actions through some combination of get and/or set functions (collectively called accessors).

You can define properties either implicitly or explicitly. One guideline is to favor implicit properties when you’re exposing a simple value; when you need custom logic when getting or setting a property value, use explicit properties instead.

Explicit Properties

Explicit properties are those where you define and control the backing store (typically with a let binding) and implement the get and set function bodies yourself. You define an explicit property with the member keyword followed by a self-identifier, the property name, a type annotation (if the compiler can’t infer it), and the function bodies, as shown here:

type Person() =

let mutable name = ""

member x.Name

with get() = name

and set(value) = name <- value

In this example, the name field is the private backing store for the read/write Name property. Once you’ve created an instance of this Person class, you can assign a value to the Name property with the assignment operator, like so:

let me = Person()

me.Name <- "Dave"

Instead of using the and keyword, you can use an alternative syntax where the get and set accessors are defined as separate properties.

type Person() =

let mutable name = ""

member x.Name with get() = name

member x.Name with set(value) = name <- value

Whichever syntax you choose, properties are public by default, but you can control their accessibility by inserting the access modifier (public, private, or internal) after the with (or and) keyword, like this:

type Person() =

let mutable name = ""

member x.Name

with public get() = name

and internal set(value) = name <- value

If you wanted the Name property to be read-only, you could revise the class to include the value as an argument to the primary constructor and remove the and set... line in this way:

type Person(name) =

member x.Name with get() = name

Of course, this is F#, so although defining a read-only property is already easy, there’s an even easier way with the explicit syntax.

type Person(name) =

member x.Name = name

When you’re creating a read-only property, the compiler automatically generates the get accessor function for you.

Implicit Properties

Implicit, or automatic, properties were added to F# in version 3.0 (if you’re using 2.0, you’ll need to use explicit properties). They’re very much like auto-implemented properties in C# in that they allow the compiler to generate the proper backing store and corresponding get/set accessor bodies. Implicit properties are a lot like their explicit counterparts, but there are a few differences.

First, implicit properties are considered part of the type’s initialization, so they must appear before other member definitions, typically along with the primary constructor. Next, they are defined via the member val keyword pair and must be initialized to a default value, as shown next. (They must not include a self-identifier.) And finally, their accessibility can be changed only at the property level, not the accessor level.

type Person() =

member val Name = "" with get, set

If your implicit property is read-only, you can omit the with expression like this:

type Person(name) =

member val Name = name

Indexed Properties

F# classes can also have indexed properties, which are useful for defining an array-like interface for working with sequential data. Indexed properties are defined like ordinary properties except that the get accessor includes an argument.

When you are creating indexed properties, naming one Item makes it a default indexed property and enables convenient syntactic support through the dot operator and a pair of brackets enclosing the index value (.[...]). For example, consider a class that accepts a string and exposes each word through a default indexer like this:

type Sentence(initial : string) =

let mutable words = initial.Split ' '

let mutable text = initial

member x.Item

with get i = words.[i]

and set i v =

words.[i] <- v

text <- System.String.Join(" ", words)

Notice that the Item property is defined like a normal property with the get, and even a set, accessor. Because this indexer is just a wrapper around the words array (String.Split returns an array), it accepts an integer value and returns the corresponding word.

F# arrays are zero-based, so you can get the second word from a sentence like this:

> let s = Sentence "Don't forget to drink your Ovaltine"

s.[1];;

val s1 : Sentence

val it : string = "forget"

To change the second word, you’d reference the index in the same way and use the assignment operator (<-) like so:

> s.[1] <- "remember";;

val it : unit = ()

> s.[1];;

val it : string = "remember"

Furthermore, default indexed properties can be multidimensional. For instance, you can define one to return a specific character from a word by including two parameters.

type Sentence(initial : string) =

-- snip --

member x.Item with get(w, i) = words.[w].[i]

Now you can easily get the first character of the second word like this:

> s.[1, 0];;

val it : char = 'f'

But what if you want to define another indexed property to get a character out of the original string? You’ve already defined a default indexed property that accepts an integer, so you can’t do it that way. In C#, you’d have to create this as a method, but in F# any property can be an indexed property. For example:

type Sentence(initial : string) =

-- snip --

member x.Chars with get(i) = text.[i]

The only caveat is that you can’t use the dot/bracket syntax that you’d use with a default indexed property; you have to access the property as if it’s a method (as described in Instance Methods) by including the index value in parentheses after the property name in this way:

> s.Chars(0);;

val it : char = 'D'

Though it looks like a method call, if the Chars indexed property included a set accessor, you’d use the assignment operator just like you would with any other property to change the underlying value.

Setting at Initialization

An alternative object initialization syntax lets you set individual property values as part of the constructor call. To use the object initialization syntax, you need only include each property name and value (separated by an equal sign) immediately following the normal constructor arguments. Let’s reconsider one of the previous Person class examples to illustrate.

type Person() =

member val Name = "" with get, set

Because the Person class has only the single, parameterless constructor, you could create an instance and then assign a value to the Name property in a second operation. But it would be much more concise to do it all at once, like this:

let p = Person(Name = "Dave")

There is one catch to using this syntax: Any properties you initialize this way must be writable.

Methods

Methods are functions that are associated with a class and that represent the type’s behavior.

Instance Methods

There are two ways to define instance methods. The first form uses the member keyword to define a public method in much the same way as a property, as demonstrated by the GetArea method that follows.

open System

type Circle(diameter : float) =

member x.Diameter = diameter

member x.GetArea() =

let r = diameter / 2.0

System.Math.PI * (r ** 2.0)

Here, the Circle class is initialized with a diameter value and contains a parameterless, public method named GetArea that calculates the area of the circle. Because GetArea is an instance method, you’ll need to create an instance of the Circle class to invoke it as follows:

> let c = Circle 5.0

c.GetArea();;

val c : Circle

val it : float = 19.63495408

Method Accessibility

As with properties, you can control access to methods with accessibility modifiers. For example, to make a method private you would simply include the private keyword in the method’s signature, as in the GetRadius method here:

type Circle(diameter : float) =

member private x.GetRadius() = diameter / 2.0

member x.Diameter = diameter

member x.GetArea() = System.Math.PI * (x.GetRadius() ** 2.0)

Alternatively, you can use a let binding to define a private function, as shown here:

type Circle(diameter : float) =

let getRadius() = diameter / 2.0

member x.Diameter = diameter

member x.GetArea() = System.Math.PI * (getRadius() ** 2.0)

Named Arguments

When you call a method, you’ll usually provide the arguments as a comma-delimited list with each argument corresponding to the parameter at the same position. For a bit of extra flexibility, though, F# allows named arguments for both methods and constructors. With named arguments, each argument is explicitly associated with a particular parameter by name. In some cases, named arguments can help clarify your code, but they also allow you to specify the arguments in any order.

The following example contains a method that calculates the Euclidean distance between two points in a three-dimensional space (RGB colors, to be exact).

open System

open System.Drawing

type ColorDistance() =

member x.GetEuclideanDistance(c1 : Color, c2 : Color) =

let getPointDistance p1 p2 = (float p1 - float p2) ** 2.0

[ getPointDistance c1.R c2.R

getPointDistance c1.G c2.G

getPointDistance c1.B c2.B ] |> List.sum |> Math.Sqrt

You can call the GetEuclideanDistance method normally by specifying two colors, or by specifying the parameter names in the argument list like this:

> let d = ColorDistance()

d.GetEuclideanDistance(Color.White, Color.Black);;

val d : ColorDistance

val it : float = 441.6729559

> d.GetEuclideanDistance(c2 = Color.White, c1 = Color.Snow);;

val it : float = 7.071067812

You can specify named arguments in any order. You can also use named arguments with unnamed arguments, but if you do, the unnamed arguments must appear first in the argument list. Finally, because named arguments are permissible only for methods defined with the member syntax, they can’t be used with functions created through let bindings.

Overloaded Methods

An overloaded method shares its name with one or more other methods in the same class but has a different set of parameters. Overloaded methods often define subsets of parameters, with each overload calling a more specific form with its supplied arguments and providing default values for others.

For example, if you were building a utility to tie in to your favorite version control system, you might define a Commit method that accepts a list of files, the description, and the target branch. To make the target branch optional, you could overload the Commit function as shown here:

open System.IO

type Repository() =

member ① x.Commit(files, desc, branch) =

printfn "Committed %i files (%s) to \"%s\"" (List.length files) desc branch

member ② x.Commit(files, desc) =

x.Commit(files, desc, ③"default")

In this example, the overload at ① is responsible for committing changes to the repository, while the overload at ② makes the branch parameter optional when you supply the default value shown at ③.

Optional Parameters

Even though F# supports method overloading, you probably won’t use it very often because F# also supports optional parameters, which are generally more convenient. If you prefix a parameter name with a question mark (?), the compiler treats it as an optional parameter.

Optional parameters are a bit different in F# than they are in C# and Visual Basic. In other languages, optional parameters are defined with a default value that’s used when the corresponding argument is omitted. In F#, though, the parameters are actually compiled to option<_> and default to None. (Optional parameter values behave like any other option type value, so you’ll still use defaultArg or pattern matching in your method to get a meaningful value, as appropriate.)

Let’s rewrite the Repository example from the previous section to use an optional parameter instead of an overloaded method.

open System.IO

type Repository() =

static member Commit(files, desc, ?branch) =

let targetBranch = defaultArg branch "default"

printfn "Committed %i files (%s) to \"%s\"" (List.length files) desc targetBranch

Although you need to manage the optional parameter within the method, you now need to maintain only the one method instead of multiple, overloaded versions. As you can see, optional parameters can reduce the likelihood of defects that come from using inconsistent defaults across overloads, and they simplify refactoring because only one method needs to change.

Slice Expressions

Indexed properties, introduced in Indexed Properties, are great for working with a single value in an encapsulated sequence, but you’ll sometimes want to work with a range of values in that sequence. Traditionally you’d have to get each item manually through the indexer, or implementIEnumerable<'T> and get the values through some combination of LINQ’s Skip and Take extension methods. Slice expressions resemble indexed properties, except that they use range expressions to identify which items should be included in the resulting sequence.

To use slice expressions with your class, you need to implement a GetSlice method. There’s really nothing special about the GetSlice method; it’s just the method that the compiler looks for when it encounters the slice expression syntax. To illustrate a slice expression, let’s revisit theSentence class from the indexed properties section.

type Sentence(initial : string) =

let words = initial.Split ' '

let text = initial

member x.GetSlice(lower, upper) =

match defaultArg lower 0 with

| l when l >= words.Length -> Array.empty<string>

| l -> match defaultArg upper (words.Length - 1) with

| u when u >= words.Length -> words.[l..]

| u -> words.[l..u]

The basic class definition is the same as before, except this time we have a GetSlice() method that accepts the lower and upper bounds. (Don’t dwell on the match expressions here; a full discussion is waiting for you in Chapter 7. For now it’s enough to know that they’re just doing some boundary checks.)

You could call this method directly in your code, but the expression form is much more convenient. For example, to retrieve the second, third, and fourth words in a sentence, you could write:

> let s = Sentence "Don't forget to drink your Ovaltine"

s.[1..3];;

val s : Sentence

val it : string [] = [|"forget"; "to"; "drink"|]

One of the nice things about slice expressions is that the bounds parameters are optional, so you can use open-ended ranges. To specify a range without a lower bound, just omit the first value (the 1) in the slice expression, which in this case is equivalent to [0..3].

> s.[..3];;

val it : string [] = [|"Don't"; "forget"; "to"; "drink"|]

Similarly, you can leave out the second parameter and get the items up to the end of the collection.

> s.[3..];;

val it : string [] = [|"drink"; "your"; "Ovaltine"|]

Like indexed properties, slice expressions can work on two dimensions, but you need to overload the GetSlice method to accept four parameters that define both pairs of lower and upper bounds. Continuing with the Sentence example, we can add a multidimensional slice overload to get a range of characters from a range of words like this:

type Sentence(initial : string) =

-- snip --

member x.GetSlice(lower1, upper1, lower2, upper2) =

x.GetSlice(lower1, upper1)

|> Array.map

(fun w -> match defaultArg lower2 0 with

| l when l >= w.Length -> ""

| l -> match defaultArg upper2 (w.Length - 1) with

| u when u >= w.Length -> w.[l..]

| u -> w.[l..u])

To use this overload, just separate the range pairs in the slice expression with a comma.

> s.[1..4, ..1];;

val it : string [] = [|"fo"; "to"; "dr"; "yo"|]

Events

The final member type is events. Events are used throughout the .NET Framework with some notable examples found in the user interface components and ADO.NET. As in other .NET languages, at their core F# events are collections of functions invoked in response to some action like a button click or an asynchronous process completion.

In many ways F# events serve the same purpose as traditional .NET events, but they’re a completely different mechanism. However, for cross-language compatibility, they can tie in to the .NET event system. (We’ll see how your custom events can harness this capability with the CLIEventattribute a bit later in this section.)

Basic Event Handling

Events in F# are instances of the Event<'T> class (found in FSharp.Core.Control). One of the primary features that the Event<'T> class enables is a more explicit publish/subscribe model than you might be used to. In this model you can subscribe to published events by adding event handlers to the event via a call to the Add function.

For example, the System.Timers.Timer class publishes an Elapsed event that you can subscribe to.

let ticks = ref 0

let t = ① new System.Timers.Timer(500.0)

t.Elapsed.Add ② (fun ea -> printfn "tick"; ticks := ticks.Value + 1)

③ t.Start()

while ticks.Value < 5 do ()

t.Dispose()

Here we create a new instance of the Timer class at ①. At ②, we subscribe to the Elapsed function using a lambda expression (an anonymous function) as the event handler. Once the timer is started at ③, the event handler prints tick and increments a reference cell’s value (remember, closures like the one created by the lambda expression can’t use mutable let bindings) every half-second, per the timer definition. When the tick counter reaches five, the loop will terminate and the timer will be stopped and disposed of.

Observing Events

The other primary benefit of F# events is that they enable you to treat events as sequences that you can intelligently partition, filter, aggregate, or otherwise act upon as they’re triggered. The Event module defines a number of functions—such as add, filter, partition, and pairwise—that accept published events.

To see this principle in action, let’s turn to an example in ADO.NET. The DataTable class triggers a variety of events in response to certain actions like changed or deleted rows. If you wanted to handle the RowChanged event, you could add a single event handler (just as in the previous section) and include logic to filter out the events you don’t care about, or you could use the filter function from the Event module and invoke your handler only when it’s needed, as follows:

open System

open System.Data

let dt = new DataTable("person")

dt.Columns.AddRange

[| new DataColumn("person_id", typedefof<int>)

new DataColumn("first_name", typedefof<string>)

new DataColumn("last_name", typedefof<string>) |]

dt.Constraints.Add("pk_person", dt.Columns.[0], true)

let 1 h1, h2 =

2 dt.RowChanged

|> 3 Event.partition

4(fun ea ->

let ln = ea.Row.["last_name"] :?> string

ln.Equals("Pond", StringComparison.InvariantCultureIgnoreCase))

5 h1.Add (fun _ -> printfn "Come along, Pond")

6 h2.Add (fun _ -> printfn "Row changed")

We’ll forego a discussion of the first half of this example; for our purposes, all that’s important there is that it sets up a DataTable with three columns and a primary key. What’s really important here is the partition function.

In this example, we invoke the partition function at ③ by supplying both a delegate (in the form of a lambda expression) at ④ and the Event object published by the DataTable’s RowChanged event at ②. The partition function then returns two new events that we bind to h1 and h2at ①. Finally, we subscribe to both of the new events by calling their Add method at ⑤ and ⑥.

Now that the table structure and event handlers are in place, we can add some rows and see how the events are triggered.

> dt.Rows.Add(1, "Rory", "Williams") |> ignore;;

Row changed

val it : unit = ()

> dt.Rows.Add(2, "Amelia", "Pond") |> ignore;;

Come along, Pond

val it : unit = ()

As you can see, when the first row is added, the last name doesn’t match the criteria specified in the filter, so h2 is triggered. However, the second row does match the criteria, so h1 is triggered instead.

If the syntax for calling the partition function looks backward, that’s because it is; the forward pipelining operator (|>) applies its left operand as the final argument to the function specified by its right operand. (The forward pipelining operator is used frequently in F#, and we’ll explore it in much more detail in Chapter 5.)

Custom Events

You can define your own custom events in your types. However, doing so is a bit different than in other .NET languages because events exist only as objects in F# and they lack keyword support.

The first thing you need to do, aside from defining the type, is create a field (with a let binding) for your event object. This is the object used to coordinate publishing and triggering the event. Once the field is defined, you can expose the event’s Publish property to the outside world with a property of your own. Finally, you’ll need to trigger the event somewhere by calling the Trigger function.

type Toggle() =

let toggleChangedEvent = Event<_>()

let mutable isOn = false

member x.ToggleChanged = toggleChangedEvent.Publish

member x.Toggle() =

isOn <- not isOn

toggleChangedEvent.Trigger (x, isOn)

With the type defined, you can create a new instance and subscribe to the ToggleChanged event as with any built-in type. For example, next we use a partition to create two new event handlers, one to handle when the toggle is turned on and another to handle when it is turned off. The call toEvent.map simply rephrases the event by throwing away the first parameter (the source, or sender, per .NET conventions) before calling the partition function.

let myToggle = Toggle()

let onHandler, offHandler =

myToggle.ToggleChanged

|> Event.map (fun (_, isOn) -> isOn)

|> Event.partition (fun isOn -> isOn)

onHandler |> Event.add (fun _ -> printfn "Turned on!")

offHandler |> Event.add (fun _ -> printfn "Turned off!")

Now every call to the Toggle method will trigger the ToggleChanged event and cause one of the two handlers to execute.

> myToggle.Toggle();;

Turned on!

val it : unit = ()

> myToggle.Toggle();;

Turned off!

val it : unit = ()

As you’ve just seen, the ToggleChanged event is fully enabled within F#. If your class won’t be consumed outside F# assemblies, you could stop here. However, if you need to use it in assemblies written in different languages, you’ll have to do one more thing: decorate theToggleChanged property with the CLIEvent attribute.

[<CLIEvent>]

member x.ToggleChanged = toggleChangedEvent.Publish

The CLIEvent attribute instructs the compiler to include the appropriate metadata that makes the event consumable from other .NET languages.

Structures

Structures, or structs, are similar to classes in that they can have fields, properties, methods, and events. Structs are defined just like classes except that the type must be decorated with the Struct attribute.

[<Struct>]

type Circle(diameter : float) =

member x.getRadius() = diameter / 2.0

member x.Diameter = diameter

member x.GetArea() = System.Math.PI * (x.getRadius() ** 2.0)

However, despite their similarities, behind the scenes, classes and structs are very different animals. The primary difference between them is that structs are value types.

This difference is significant because it affects not only how you interact with the data but also how value types are represented in the computer’s memory. With both types, the runtime allocates space in memory to store the value. Value types always result in a new allocation with the data copied into that space. With reference types, the memory is allocated once and accessed via a reference that identifies its location.

When you pass a reference type to a function, the runtime creates a new reference to that location in memory rather than a copy of the data. Therefore, reference types can more easily wreak havoc through side effects, because when you pass a reference type to a function any changes that you make to that object are immediately reflected wherever that object is referenced. In contrast, passing a value type to a function creates a copy of the value so any changes to it are isolated to that one instance.

Structs are also initialized differently than classes. Unlike classes, the compiler generates a default (parameterless) constructor for structs that initializes all fields to their appropriate zero value (zero, null, and so on). This means that you can’t use let bindings to create private instance fields or methods within a struct unless they’re static; instead, you must use val to define struct instance fields. Also, you can’t define your own default constructor, so any additional constructors you define must accept at least one parameter. (Your fields can still be mutable as long as you don’t include a primary constructor.)

Because of differences in how memory is allocated for reference and value types, structs cannot contain fields of their own type. Without this restriction, the memory requirement for a struct instance would be infinitely large because each instance would recursively require enough space for another instance of the same type.

Finally, structs can implement interfaces but cannot otherwise participate in inheritance. Regardless, structs still derive from System.Object, so you can override methods (like ToString).

Inheritance

In OO programming, inheritance describes an identity relationship between two types in the way that an apple is a fruit. F# classes support single inheritance, meaning that any given class can directly inherit from only one other in order to establish a class hierarchy. Through inheritance, public (and sometimes internal) members exposed by the base type are automatically available in the derived type. You can see this principle in action in the following snippet.

type BaseType() =

member x.SayHello name = printfn "Hello, %s" name

type DerivedType() =

inherit BaseType()

The DerivedType defined here doesn’t define any functionality of its own, but because it derives from BaseType, the SayHello method is accessible through DerivedType.

F# inheritance requires a primary constructor. To specify a base class, include the inherit keyword followed by the base type name and its constructor arguments in the primary constructor before any bindings or member definitions. For instance, a task management system might have aWorkItem class that represents all work items in the system, as well as specialized classes such as Defect and Enhancement that derive from the WorkItem class, as shown next in bold.

type WorkItem(summary : string, desc : string) =

member val Summary = summary

member val Description = desc

type Defect(summary, desc, severity : int) =

inherit WorkItem(summary, desc)

member val Severity = severity

type Enhancement(summary, desc, requestedBy : string) =

inherit WorkItem(summary, desc)

member val RequestedBy = requestedBy

Every .NET class, including the primitive types, ultimately participates in inheritance. Also, when you define a class without explicitly specifying a base class, the defined class implicitly inherits from System.Object.

Casting

In Chapter 3 you learned how to convert between numeric types. Types can also be converted within their type hierarchy through the upcast and downcast operators.

Upcasting

Until now I’ve maintained that there are no implicit conversions in F#, but that’s not entirely true. The only time that types are implicitly upcast (converted to a type higher in their inheritance structure) is when they’re passed to a method or a let-bound function where the corresponding parameter is a flexible type. In all other cases, you must explicitly cast the type with the static cast operator (:>).

To see the static cast operator in action, let’s continue with the WorkItem example by creating a Defect and immediately casting it to a WorkItem.

> let w = Defect("Incompatibility detected", "Delete", 1) :> WorkItem;;

val w : WorkItem

The static cast operator resolves valid casts at compile time. If the code compiles, the conversion will always succeed.

Downcasting

The opposite of an upcast is a downcast. Downcasts are used to convert a type to something lower in its hierarchy, that is, to convert a base type to a derived type. To perform a downcast, you use the dynamic cast operator (:?>)

Because the WorkItem instance we created in the previous example is still a Defect, we can use the dynamic cast operator to convert it back to a WorkItem.

> let d = w :?> Defect;;

val d : Defect

Unlike the static cast operator, the dynamic cast operator isn’t resolved until run time, so you may see an InvalidCastException if the target type isn’t valid for the source object. For instance, if you try to downcast w to Enhancement, the cast will fail.

> let e = w :?> Enhancement;;

System.InvalidCastException: Unable to cast object of type 'Defect' to type 'Enhancement'.

at Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicFunctions.UnboxGeneric[T](Object source)

at <StartupCode$FSI_0007>.$FSI_0007.main@()

Stopped due to error

Overriding Members

Aside from reusing code, you might use inheritance to change the functionality offered by a base class by overriding its members.

For example, the ToString method defined on System.Object is a great (and often overlooked) debugging tool whose default implementation isn’t particularly informative because it just returns the type name. To make it more useful, your classes can override the default functionality and return a string that actually describes the object.

To illustrate, consider the WorkItem class from earlier. If you were to call its ToString method, you would see something like this:

> let w = WorkItem("Take out the trash", "It's overflowing!")

w.ToString();;

val w : WorkItem

val it : string = "FSI_0002+WorkItem"

NOTE

In the preceding example, FSI_0002+ is an artifact of invoking the code in FSI. Your type name will probably differ.

To override the default behavior and make ToString return something more useful, define a new method with the override keyword.

type WorkItem(summary : string, desc : string) =

-- snip --

override x.ToString() = sprintf "%s" x.Summary

If you call ToString now, the result will be the summary text instead of the type name.

> let w = WorkItem("Take out the trash", "It's overflowing!")

w.ToString();;

val w : WorkItem = Take out the trash

val it : string = "Take out the trash"

You can override a given function only once per type, but you can override it at multiple levels in the hierarchy. For instance, here’s how you could override ToString again in the Defect class to display the severity of the defect:

type Defect(summary, desc, severity : int) =

inherit WorkItem(summary, desc)

member val Severity = severity

override x.ToString() = sprintf "%s (%i)" x.Summary x.Severity

When overriding a virtual member (an abstract member with a default implementation), you can call into the base functionality through the base keyword. The base keyword behaves like a self-identifier except that it represents the base class.

Continuing with our ToString override theme, to augment the default behavior your override could call base.ToString() like this:

type Defect(summary, desc, severity : int) =

-- snip --

override x.ToString() =

sprintf "%s (%i)" (base.ToString()) x.Severity

Note that the base keyword is available only in classes that explicitly inherit from another type. To use the base keyword in a class that inherits from System.Object, you would need to explicitly inherit from it as follows:

type WorkItem(summary : string, desc : string) =

inherit System.Object()

-- snip --

override x.ToString() =

sprintf "[%s] %s" (base.ToString()) x.Summary

Abstract Classes

An abstract class is one that can’t be directly instantiated; it’s accessible only through derived classes. Abstract classes typically define a common interface and optional implementation for a group of related classes that fulfill similar needs in different ways. Abstract classes are used throughout the .NET Framework; one great example is the TextWriter class in the System.IO namespace.

The TextWriter class defines a common mechanism for writing characters to something. It doesn’t care where or how the characters are written, but it orchestrates some of the process, with the implementation details left to individual derived classes such as StreamWriter,StringWriter, and HttpWriter.

You can define your own abstract classes by decorating the type definition with the AbstractClass attribute. For example, to create a simple tree structure you could use an abstract class as follows:

[<AbstractClass>]

type Node(name : string, ?content : Node list) =

member x.Name = name

member x.Content = content

Abstract Members

One reason to define an abstract class is to define abstract members, that is, members without an implementation. Abstract members are allowed only in abstract classes (or interfaces, described in Interfaces) and must be implemented in a derived class. They’re handy when you want to define what a class does but not how it does it.

Abstract Properties

When you want to define the data associated with a particular type but not how that data is stored or what happens when it is accessed, you can define an abstract property with the abstract keyword.

For example, this abstract class contains one abstract property:

[<AbstractClass>]

type AbstractBaseClass() =

abstract member SomeData : string with get, set

AbstractBaseClass requires only that its subtypes implement the SomeData property, but they’re free to implement their own storage mechanism. For instance, one derived class may use a traditional backing store, whereas another may opt to use a .NET generic dictionary as follows:

type BindingBackedClass() =

inherit AbstractBaseClass()

let mutable someData = ""

override x.SomeData

with get() = someData

and set(v) = someData <- v

type DictionaryBackedClass() =

inherit AbstractBaseClass()

let dict = System.Collections.Generic.Dictionary<string, string>()

[<Literal>]

let SomeDataKey = "SomeData"

override x.SomeData

with get() =

match dict.TryGetValue(SomeDataKey) with

| true, v -> v

| _, _ -> ""

and set(v) =

match System.String.IsNullOrEmpty(v) with

| true when dict.ContainsKey(SomeDataKey) ->

dict.Remove(SomeDataKey) |> ignore

| _ -> dict.[SomeDataKey] <- v

As you can see, both BindingBackedClass and DictionaryBackedClass derive from AbstractBaseClass, but they implement the SomeData property in very different ways.

Abstract Methods

Even though you can define abstract properties, you’re much more likely to use abstract methods. Like abstract properties, abstract methods allow you to define a capability that derived classes must implement without specifying any of the implementation details. For example, when calculating the area of a shape, you might define an abstract Shape class that includes an abstract GetArea method.

[<AbstractClass>]

type Shape() =

abstract member GetArea : unit -> float

Because the method doesn’t have an implementation, you must explicitly define the entire signature. In this case, the GetArea method accepts unit and returns a float.

Overriding a method is also similar to overriding a property, as you can see in the following Circle and Rectangle classes:

open System

type Circle(r : float) =

inherit Shape()

member val Radius = r

override x.GetArea() =

Math.Pow(Math.PI * r, 2.0)

type Rectangle(w : float, h : float) =

inherit Shape()

member val Width = w

member val Height = h

override x.GetArea() = w * h

Virtual Members

Like C# and Visual Basic, F# allows virtual members—that is, properties or methods that can be overridden in a derived class. But unlike other .NET languages, F# takes a more literal approach to virtual members. For instance, in C# you include the virtual modifier in a nonprivate instance member definition, and in Visual Basic you use the Overridable modifier to achieve the same effect.

Virtual members in F# are closely related to abstract members. In fact, in order to create a virtual member you first define an abstract member and then provide a default implementation with the default keyword. For example, in the following listing the Node class is the basis for a simple tree structure. It provides two virtual methods, AddChild and RemoveChild, which help control the tree structure.

open System

open System.Collections.Generic

type Node(name : string) =

let children = List<Node>()

member x.Children with get() = children.AsReadOnly()

abstract member AddChild : Node -> unit

abstract member RemoveChild : Node -> unit

default x.AddChild(n) = children.Add n

default x.RemoveChild(n) = children.Remove n |> ignore

With this definition, all Node class instances (including any derived types) will allow children. To create a specialized Node that doesn’t allow children, you could define a TerminalNode class and override both virtual methods to prevent children from being added or removed.

type TerminalNode(name : string) =

inherit Node(name)

[<Literal>]

let notSupportedMsg = "Cannot add or remove children"

override x.AddChild(n) =

raise (NotSupportedException(notSupportedMsg))

override x.RemoveChild(n) =

raise (NotSupportedException(notSupportedMsg))

Sealed Classes

A sealed class is a class that cannot serve as the base class for another class. One of the most notable sealed classes in the .NET Framework is System.String.

You can create your own sealed classes by decorating them with the Sealed attribute, as shown in the following snippet:

[<Sealed>]

type NotInheritable() = class end

If you tried to create another class that inherits from the NotInheritable class, the compiler would raise an error like this:

> type InvalidClass() =

inherit NotInheritable();;

inherit NotInheritable();;

--^^^^^^^^^^^^^^^^^^^^^^^^

stdin(4,3): error FS0945: Cannot inherit a sealed type

Static Members

Fields, properties, and methods are instance members by default. You can make each static so that it applies to the type rather than a specific instance by including the static keyword before the member definition.

A WORD ABOUT STATIC CLASSES

In C# a static class is an implicitly sealed class that cannot be instantiated and in which all members are static. Most of the time in F#, when you want static class–like functionality, you’ll place it in a module. However, modules have certain limitations. For example, they don’t allow you to overload functions.

Although F# doesn’t directly support static classes the way that C# does, you can do a little syntactic dance to achieve a similar effect. To do so, omit the primary constructor (or make it private if you need a static initializer) to ensure that no instances can be created, and then verify that every member is static (the F# compiler won’t enforce this for you). For completeness, decorate the class with SealedAttribute so that nothing inherits from it.

Static Initializers

Static initializers, or static constructors, execute only once per class and ensure that certain code is executed before a class is used for the first time. You create static initializers in F# through a series of static let and do bindings, just as you would when defining a primary constructor. In fact, if your class needs a static initializer, you must include a primary constructor to contain the static bindings as shown here:

type ClassWithStaticCtor() =

static let mutable staticField = 0

static do printfn "Invoking static initializer"

staticField <- 10

do printfn "Static Field Value: %i" staticField

Static initializers can access only the static members of their containing class. If you try to access an instance member from within a static initializer, you’ll get a compiler error.

Static Fields

Static fields are often useful as a single reference for something you need to use repeatedly. For example, to associate certain data with the class itself, define a static field by including the static keyword before a let binding, as shown here:

module Logger =

let private log l c m = printfn "%-5s [%s] %s" l c m

let LogInfo = log "INFO"

let LogError = log "ERROR"

type MyService() =

static let logCategory = "MyService"

member x.DoSomething() =

Logger.LogInfo logCategory "Doing something"

member x.DoSomethingElse() =

Logger.LogError logCategory "Doing something else"

When the DoSomething and DoSomethingElse methods are called, each calls a function in the Logger module to write a log message in the same category but without the duplication of data.

> let svc = MyService()

svc.DoSomething()

svc.DoSomethingElse();;

INFO [MyService] Doing something

ERROR [MyService] Doing something else

Static Properties

Properties can also be static. Here, a read-only static property is used to expose the number of times a particular method has been called across all instances of your class.

type Processor() =

static let mutable itemsProcessed = 0

static member ItemsProcessed = itemsProcessed

member x.Process() =

itemsProcessed <- itemsProcessed + 1

printfn "Processing..."

Every time the Process method is called, it increments the itemsProcessed field and prints a message. To see how many times the Process method has been called across all instances, inspect the ItemsProcessed property on the Processor class itself.

> while Processor.ItemsProcessed < 5 do (Processor()).Process();;

Processing...

Processing...

Processing...

Processing...

Processing...

val it : unit = ()

This example iterates as long as the Process method has been invoked fewer than five times. Each iteration creates a new instance of the Processor class and invokes its Process method (which illustrates how the static property is instance agnostic).

Static Methods

Like other static members, static methods apply to a type rather than an instance. For example, static methods are commonly used in the Factory pattern (a common approach to creating instances of similar classes without relying on a specific implementation). In some variations of the Factory pattern, a static method returns new instances of objects that conform to a specific interface. To illustrate this concept, consider an application where you need to handle different image formats. You may have an abstract ImageReader class that other types derive from in order to handle specific formats like JPEG, GIF, and PNG.

[<AbstractClass>]

type ImageReader() =

abstract member Dimensions : int * int with get

abstract member Resolution : int * int with get

abstract member Content : byte array with get

type JpgImageReader(fileName : string) =

inherit ImageReader()

-- snip --

type GifImageReader(fileName : string) =

inherit ImageReader()

-- snip --

type PngImageReader(fileName : string) =

inherit ImageReader()

-- snip --

A Factory method for creating instances of these classes might look something like this:

open System.IO

[<Sealed>]

type ImageReaderFactory private() =

static member CreateReader(fileName) =

let fi = FileInfo(fileName)

match fi.Extension.ToUpper() with

| ".JPG" -> JpgImageReader(fileName) :> ImageReader

| ".GIF" -> GifImageReader(fileName) :> ImageReader

| ".PNG" -> PngImageReader(fileName) :> ImageReader

| ext -> failwith (sprintf "Unsupported extension: %s" ext)

The static CreateReader method in the preceeding snippet uses F# pattern matching to create the appropriate ImageReader implementation based on the provided filename. When the file extension isn’t recognized, it raises an exception indicating that the format isn’t supported. Because the method is static, you can call it without creating an instance of the ImageReaderFactory class, as shown here:

ImageReaderFactory.CreateReader "MyPicture.jpg"

ImageReaderFactory.CreateReader "MyPicture.gif"

ImageReaderFactory.CreateReader "MyPicture.png"

ImageReaderFactory.CreateReader "MyPicture.targa"

Mutually Recursive Types

When two or more types depend on each other such that one cannot be used without the other, the types are said to be mutually recursive.

To illustrate, think of a book and its pages. The book can contain a collection of pages, but each page might also refer back to the book. Remember, F# is evaluated top-down, so which type would you define first? The book or the page? Because the book depends on its pages and the page refers back to the book, there is mutual recursion here. This means that you must define the types together using the and keyword, as shown here:

type Book() =

let pages = List<Page>()

member x.Pages with get() = pages.AsReadOnly()

member x.AddPage(pageNumber : int, page : Page) =

if page.Owner = Some(x) then failwith "Page is already part of a book"

pages.Insert(pageNumber - 1, page)

and Page(content : string) =

let mutable owner : Book option = None

member x.Content = content

member x.Owner with get() = owner

member internal x.Owner with set(v) = owner <- v

override x.ToString() = content

Interfaces

In OO programming, interfaces specify the properties, methods, and sometimes even events that a type must support. In some ways interfaces are like abstract classes, with certain important differences. For one, unlike abstract classes, interfaces cannot contain any implementations of their members; their members must be abstract. Also, because interfaces define functionality that implementers must support, all interface members are implicitly public. Finally, interfaces aren’t subject to the same inheritance restrictions as classes: A class can implement any number of interfaces (and structs can, too).

Implementing Interfaces

F# approaches interface implementation a bit differently than its .NET language counterparts. C# and Visual Basic allow both implicit and explicit implementations. With implicit implementations, interface members are accessible directly through the implementing class, whereas with explicit implementations, interface members are accessible only when the implementing type is treated as the interface.

Consider this C# example with two classes that both implement the IDisposable interface:

// C#

class ImplicitExample : IDisposable

{

public void Dispose()

{

Console.WriteLine("Disposing");

}

}

class ExplicitExample : IDisposable

{

void IDisposable.Dispose()

{

Console.WriteLine("Disposing");

}

}

Both classes implement IDisposable, but ImplicitExample① does so implicitly and ExplicitExample ② does it explicitly. This difference has a dramatic effect on how you call the Dispose method in each class, as shown here:

// C#

var ex1 = ①new ImplicitExample();

② ex1.Dispose();

var ex2 = ③ new ExplicitExample();

④ ((IDisposable)ex2).Dispose();

Here we instantiate ImplicitExample at ① and ExplicitExample at ③. For both classes we call the Dispose method, but because Dispose is implicitly implemented in the ImplicitExample class we can call it directly through ex1, as we do at ②. The compiler would produce an error if we tried the same approach with ex2 because Dispose is explicitly implemented in ExplicitExample. Instead, we need to cast ex2 to IDisposable, as shown at ④, in order to call its Dispose method.

NOTE

All interface implementations in F# are explicit. Though F# honors implicit interface implementations on types defined in other languages, any implementations that you define in F# will be explicit.

Implementing an interface in F# is similar to inheriting from another class except that it uses the interface keyword. For example, to implement IDisposable in one of your types, you could do this:

open System

type MyDisposable() =

interface IDisposable with

member x.Dispose() = printfn "Disposing"

To manually invoke the Dispose method on the MyDisposable class, you’ll need to cast an instance to IDisposable, as shown here with the static cast operator:

let d = new MyDisposable()

(d :> IDisposable).Dispose()

Defining Interfaces

When you define a type without any constructors and only abstract members, the F# compiler infers that the type is an interface. For example, an interface for working with image data might look something like this:

open System.Drawing

open System.IO

type IImageAdapter =

abstract member PixelDimensions : SizeF with get

abstract member VerticalResolution : int with get

abstract member HorizontalResolution : int with get

abstract member GetRawData : unit -> Stream

As you can see, the IImageAdapter type contains no constructors and all of its four members are abstract. To define an empty, or marker, interface you can end the definition with the interface end keyword pair:

type IMarker = interface end

NOTE

It’s standard practice in .NET development to begin interface names with a capital letter I . You should do so for the sake of consistency.

Like classes, interfaces can inherit from each other to define more specialized contracts. Also like classes, interface inheritance is accomplished with the inherit keyword.

Let’s continue our imaging example. The IImageAdapter interface is helpful for working with any image format, but some formats include capabilities not available in others. To handle these, you could define additional interfaces that represent these capabilities. For example, when working with a format that supports transparency you might create an ITransparentImageAdapter that derives from IImageAdapter, as shown here:

type ITransparentImageAdapter =

inherit IImageAdapter

abstract member TransparentColor : Color with get, set

Now, any types that implement the ITransparentImageAdapter must implement all members defined by both IImageAdapter and ITransparentImageAdapter.

Custom Operators

In Chapter 3 you saw numerous predefined operators for working with the built-in data types. You can use operator overloading to extend many of these to your types as well. By overloading operators, you can make your custom types interact a bit more naturally.

Operators in F# come in two forms: prefix and infix. Prefix operators are placed before their operand, whereas infix operators are placed between their operands. F# operators can also be unary or binary, meaning that they operate against one or two arguments, respectively. Custom operators are defined as static methods except that the name is the operator wrapped in parentheses.

Prefix Operators

When defining a prefix operator, you must begin its name with a tilde (~) to distinguish it from infix operators with the same name. The tilde is not otherwise part of the operator. To demonstrate operator overloading, we’ll define a type that represents basic RGB colors. Consider this class definition:

type RgbColor(r, g, b) =

member x.Red = r

member x.Green = g

member x.Blue = b

override x.ToString() = sprintf "(%i, %i, %i)" r g b

To calculate the negative color you could define a GetNegative function, but wouldn’t it be more intuitive to prefix an instance with the negative sign (-) instead?

type RgbColor(r, g, b) =

-- snip --

/// Negate a color

static member (~-) (r : RgbColor) =

RgbColor(

r.Red ^^^ 0xFF,

r.Green ^^^ 0xFF,

r.Blue ^^^ 0xFF

)

With the custom operator defined, you can now create a color instance and find its negative with a convenient syntax like this:

> let yellow = RgbColor(255, 255, 0)

let blue = -yellow;;

val yellow : RgbColor = (255, 255, 0)

val blue : RgbColor = (0, 0, 255)

Infix Operators

Creating infix operators is almost like creating prefix operators except that you omit the tilde character from the name.

Continuing with the RgbColor example, it would be nice to add and subtract two colors using the familiar and natural + and - operators.

open System

type RgbColor(r, g, b) =

-- snip --

/// Add two colors

static member (+) (l : RgbColor, r : RgbColor) =

RgbColor(

Math.Min(255, l.Red + r.Red),

Math.Min(255, l.Green + r.Green),

Math.Min(255, l.Blue + r.Blue)

)

/// Subtract two colors

static member (-) (l : RgbColor, r : RgbColor) =

RgbColor(

Math.Max(0, l.Red - r.Red),

Math.Max(0, l.Green - r.Green),

Math.Max(0, l.Blue - r.Blue)

)

Now we can add and subtract colors just as we would add and subtract numbers.

> let red = RgbColor(255, 0, 0)

let green = RgbColor(0, 255, 0)

let yellow = red + green;;

val red : RgbColor = (255, 0, 0)

val green : RgbColor = (0, 255, 0)

val yellow : RgbColor = (255, 255, 0)

> let magenta = RgbColor(255, 0, 255)

let blue = magenta - red;;

val magenta : RgbColor = (255, 0, 255)

val blue : RgbColor = (0, 0, 255)

New Operators

You’re not limited to overloading only existing operators. You can define custom operators using various combinations of the characters !, %, &, *, +, -, ., /, <, =, >, ?, @, ^, |, and ~. Creating custom operators can be complicated because the combination you select determines the precedence (priority) and associativity (right to left or left to right) of the operation. Furthermore, creating custom operators can hinder the comprehensibility of your code if you choose something that’s not intuitive. That said, if you still want to define a new operator, the definition looks the same as an overload.

For example, in the previous section we overloaded the + operator to add two colors, but how about blending colors? The + operator would have been a nice choice for a blending operation, but because it’s already being used for adding colors we can define the += operator instead.

type RgbColor(r, g, b) =

-- snip --

/// Blend two colors

static member (+=) (l : RgbColor, r : RgbColor) =

RgbColor(

(l.Red + r.Red) / 2,

(l.Green + r.Green) / 2,

(l.Blue + r.Blue) / 2

)

Now blending two colors is as easy as adding them:

> let grey = yellow += blue;;

val grey : RgbColor = (127, 127, 127)

Global Operators

Not only does F# allow you to overload operators on types, but you can also define operators globally. This lets you create new operators even for types you don’t control! For example, to define any of the custom operators on the standard System.Drawing.Color struct, you could define a new operator at the global level using a let binding as follows:

open System

open System.Drawing

let (+) (l : Color) (r : Color) =

Color.FromArgb(

255, // Alpha channel

Math.Min(255, int <| l.R + r.R),

Math.Min(255, int <| l.G + r.G),

Math.Min(255, int <| l.B + r.B)

)

WARNING

Be careful when defining global operators. Any operator you define that conflicts with the built-in one will take priority, meaning you can inadvertently replace core functionality.

Object Expressions

As an alternative to formal inheritance, F# provides object expressions, a handy construct for creating ad hoc (anonymous) types based on an existing class or interface. Object expressions are useful when you need a one-off type but don’t want to create a formal type. (Although the analogy isn’t perfect, you might find it helpful to think of object expressions as lambda expressions for types, because the result of an object expression is an instance of a new type that implements the interface or inherits from a base class.)

For example, consider a simplified game scenario where a character can equip a weapon. You might see a weapon interface and character class like this:

type IWeapon =

abstract Description : string with get

abstract Power : int with get

type Character(name : string, maxHP : int) =

member x.Name = name

member val HP = maxHP with get, set

member val Weapon : IWeapon option = None with get, set

member x.Attack(o : Character) =

let power = match x.Weapon with

| Some(w) -> w.Power

| None -> 1 // fists

o.HP <- System.Math.Max(0, o.HP - power)

override x.ToString() =

sprintf "%s: %i/%i" name x.HP maxHP

You can use these definitions to create a few characters:

let witchKing = Character("Witch-king", 100)

let frodo = Character("Frodo", 50)

As currently written, if either character attacked the other he wouldn’t do much damage since he has only his fists. It would be nice to give each character a weapon, but all we have right now is the IWeapon interface. We could define types for every weapon we can think of, but it’s much more convenient to write a function that creates weapons for us via an object expression.

Object expressions, like the one in the following forgeWeapon function, are defined with the new keyword followed by the type name, the with keyword, and the member definitions all wrapped in braces.

let forgeWeapon desc power =

{ new IWeapon with

member x.Description with get() = desc

member x.Power with get() = power }

With the forgeWeapon function in place, we can create some weapons for our characters.

> let morgulBlade = forgeWeapon "Morgul-blade" 25

let sting = forgeWeapon "Sting" 10;;

val morgulBlade : IWeapon

val sting : IWeapon

As you can see, both calls to forgeWeapon result in new instances of IWeapon. They can be used as if they had been formally defined through type definitions, as you can see by assigning each to a character and invoking the Attack function:

witchKing.Weapon <- Some(morgulBlade)

frodo.Weapon <- Some(sting)

witchKing.Attack frodo

Despite their convenience, object expressions aren’t suitable for every situation. One of their primary drawbacks is that they must implement every abstract member from the underlying type. If the underlying interface or base class has many abstract members, an object expression can become cumbersome very quickly, so you would probably want to consider using a different construct.

Object expressions aren’t limited to a single base type. To implement multiple base types with an object expression, you use an inheritance-like syntax. For instance, if you wanted weapons created through the forgeWeapon function to also implement IDisposable, you could use the following:

let forgeWeapon desc power =

{ new IWeapon with

member x.Description with get() = desc

member x.Power with get() = power

interface System.IDisposable with

member x.Dispose() = printfn "Disposing" }

Creating a new weapon is the same as earlier:

let narsil = forgeWeapon "Narsil" 25

Objects created through object expressions that include multiple base types are always treated as the type listed immediately after the new keyword, unless they’re explicitly cast to one of the other types. For example, in the case of the forgeWeapon function, the returned object will beIWeapon unless you cast it to IDisposable.

(narsil :?> System.IDisposable).Dispose()

Type Extensions

When LINQ was added to the .NET Framework, one exciting feature that it introduced to C# and Visual Basic was extension methods. Extension methods allow you to add new methods to an existing type without relying on inheritance or other design patterns such as the Decorator pattern. F# provides similar capabilities except that it doesn’t stop with methods. In F#, you can create extension methods, properties, events, and even static members!

You extend existing types in F# through type extensions, or type augmentations. Type extensions come in two flavors: intrinsic and optional.

Intrinsic extensions must be defined in the same namespace or module, and in the same source file as the type being extended. The new extensions become part of the extended type when the code is compiled and are visible through reflection. Intrinsic extensions are useful when you want to build up a type incrementally by grouping related pieces or as an alternative to building mutually recursive type definitions.

Optional extensions must be defined in a module. Like their C# and Visual Basic counterparts, they are accessible only when their containing namespace or module is open but are not visible through reflection. Optional extensions are most useful for adding custom functionality to types you don’t control or that are defined in other assemblies.

Regardless of whether you’re defining intrinsic or optional extensions, the syntax is the same. You begin with a new type definition. The difference is that instead of using a primary constructor and an equal sign, you use the with keyword followed by your extension definitions. For example, here we extend the Color struct (in System.Drawing) with both a static and an instance method.

module ColorExtensions =

open System

open System.Drawing

open System.Text.RegularExpressions

// Regular expression to parse the ARGB components from a hex string

① let private hexPattern =

Regex("^#(?<color>[\dA-F]{8})$", RegexOptions.IgnoreCase ||| RegexOptions.Compiled)

// Type extension

② type Color with

③ static member FromHex(hex) =

match hexPattern.Match hex with

| matches when matches.Success ->

Color.FromArgb <| Convert.ToInt32(matches.Groups.["color"].Value, 16)

| _ -> Color.Empty

④ member x.ToHex() = sprintf "#%02X%02X%02X%02X" x.A x.R x.G x.B

This optional type extension enhances the Color struct’s usability by allowing you to create new instances from known hexadecimal color strings or translate the color into a hexadecimal color string. The type extension itself is at ②. The static extension method ③ relies on the regular expression (a domain-specific language for parsing strings) at ① to match and extract the hexadecimal value to convert it into the ARGB value passed to Color’s constructor. The instance extension method ④ simply returns the ARGB value formatted as a hexadecimal string.

CROSS-LANGUAGE CONSIDERATIONS

Despite serving a similar purpose, extension methods in F# are implemented differently than in the rest of the .NET Framework. Therefore, optional extension methods defined in F# aren’t accessible as extension methods in C# or Visual Basic unless you include the Extension attribute in both the type definition and extension methods.

Summary

Despite F#’s perception as a niche functional language on the .NET Framework, you’ve seen in this chapter that F# is also a full-featured OO language. Numerous examples demonstrated how F#’s concise syntax aids you in developing robust OO frameworks complete with classes, structures, and interfaces. You’ve even seen how to implement some common design patterns like Singleton and Factory.

Although F# supports the same common OO concepts as its more established counterpart languages, you’ve learned how it takes familiar concepts like operator overloading, events, and extension methods and expands them into something much more powerful through observation and type augmentation. Finally, you’ve seen how entirely new constructs like object expressions can improve code quality by allowing you to create ad hoc types when and where you need them.