Introduction
For the past 15 years or so, object-oriented (OO) design
(OOD) and programming (OOP) has been the lingua franca of mainstream
development. As a multidisciplinary language, F# supports a full complement of
OOP constructs including structures, classes, interfaces, inheritance, etc. In
this chapter, we’ll look at how to work with existing .NET types, as well as
how to build our own OO types using the mechanisms F# supplies.
Please note that the chapters on OOP assume familiarity
with the concepts and terminology of encapsulation, classes, object instances,
inheritance, and polymorphism.
Access Specifiers
F# supports common OO access specifiers, which give
structures (and classes, etc.) a way to control how clients can interact with
them. F# supports the following access control keywords:
·
public – all callers can access
·
internal – callers within the same assembly
have access
·
private – callers within the same enclosing
type or module can access
If you’ve been programming in OO languages for any length of
time, your immediate question is “What about protected access?” F# may support
the protected
keyword in the future; however, if you try to use it in code today, the F#
compiler will tell you it’s reserved for future use; therefore, as of this
writing, this is no protected access. We use access specifiers
throughout the rest of this chapter and throughout the rest of this book.
Structures
The first step down the road toward OOP takes us face to
face with structures. A structure is a logically-related set of data and
methods that exhibits a low-overhead representation on the stack. A structure
is a .NET value type, which means it uses pass-by-value semantics, i.e., a copy
is made when the structure is passed around. In the following example, we
define a simple structure Book containing several simple fields:
[<Struct>]
type Book =
val Author: string
val Title: string
val Price: float
All structures are decorated with the [<Struct>]
attribute, which instructs the F# compiler to apply value semantics.
Using Fields
Structures are made up of fields, constructors, and
optionally, interface implementations. To declare fields, we use the keyword val.
In the example above, the structure Book defines three
fields: Author, Title and Price. Fields may be static
and may be declared public, internal or private
(see the next section of this chapter for the details on these access
specifiers). The default access for fields is public.
Fields may also be mutable. The general form of the val
expression is:
[ static ] val [ mutable ] [ access-modifier ]
field-name : type-name
To access a structure’s fields, you use dot (.)
notation.
Creating Instances
Now that we have a structure, let’s talk about how to work
with it. In general, we work with structures by creating instances of them via
the keyword new, as demonstrated below:
let b = new Book()
This creates a new instance of the Book structure and
assigns it to the identifier b. When F# creates the
new book instance b, it needs to initialize the three fields Author,
Title,
and Price.
To initialize these field, F# needs to invoke a constructor. The first thing F#
will do is to look for a custom constructor. If it finds one, it will invoke
it. If is does not find one, it will use the default constructor that it has
generated on our behalf behind the scenes. This defaul constructor initializes
all numeric types to 0 and all non-numeric types to null.
Since we have not yet defined a custom constructor for Book, F# invokes the
default constructor and sets Author to null,
Title
to null
and Price
to 0.0.
Mutability
Since Book’s fields are not specified as mutable,
they will remain at their default values throughout their existence. If you
need the ability to modify a field over the lifetime of the application, you
must declare it mutable, as in the following example:
[<Struct>]
type
Book =
val Author: string
val Title: string
val mutable
Price: float // now, we can adjust Price
Given this new definition of the Book
structure, we can create instances of Book that allow the Price
field to change over time. In order to actually modify mutable
fields, the structure instance itself must be created using the mutable
keyword, as in the following example:
let mutable b = new
Book() // note use of "mutable b" here
b.Price
<- 11.50 // updated via destructive
assignment
Mutability is granted on a field-by-field basis. Given the
code above, the following line will generate a compiler error:
b1.Author <- "Jones"
// error - Author not mutable
Custom Constructors
In addition to fields, structures can define one or more
custom constructors that are used to initialize their fields to default values.
As mentioned previously, if you choose not to supply a custom constructor, the
F# compiler will generate one on your behalf.
A custom constructor is defined using the keyword new
followed by a list of arguments. The arguments may include type information;
however, if they do not, F# type inference will try to determine their types
based on how they’re used in the body of the constructor.
In the following example, we update the Book
structure to include an explicit, custom constructor:
[<Struct>]
type
Book =
val Author: string
val Title: string
val Price: float
new(a, t, p) = { Author = a;
Title = t; Price = p }
A custom constructor enables a caller to pass in
initialization arguments when creating a new structure instance, as shown here:
let b = new Book("Jones", "Happy
Days", 11.45)
Note that the structure’s fields do not need to be
declared mutable in order to be initialized by the constructor.
Explicit constructors can contain whatever arguments you
need in order to initialize your structure. The only requirement is that the
body of the constructor must initialize all of the fields. If your custom
constructor fails to initialize all of the structure’s fields, the compiler
will generate an error.
You can create multiple custom constructors, each taking a
different set of arguments. Multiple constructors provide callers with a
variety of creation options. As an example, let’s update our Book
structure to include two custom constructors:
[<Struct>]
type
Book =
val Author: string
val Title: string
val Price: float
new(a, t, p) = { Author = a; Title = t; Price =
p }
new(a, t) = { Author = a; Title =
t; Price = 12.50 }
Given the above definition, we can now create book instances
as follows:
let wt =
new Book("Where
the Wild Things Are", "Maurice
Sendak", 9.50)
let dr = new Book("The Girl with the Dragon Tattoo", "Stieg Larsson")
Of course, you can combine custom constructors with mutable
fields, as in our final example:
[<Struct>]
type
Book =
val Author: string
val mutable
Title: string
val mutable
Price: float
new(a, t, p) = { Author = a; Title = t; Price =
p }
let mutable b1 = new
Book("Smythe", "Money and You", 10.00)
b1.Title
<- "Money and Your Life"
b1.Price <- 11.75
General Form
For the studious among us, a structure in F# takes the
following form:
[ attributes ]
[<StructAttribute>]
type [accessibility-modifier] type-name =
type-definition-elements
Restrictions
A few restrictions to keep in mind – structures cannot
participate in inheritance, cannot contain let or do
bindings (we’ll see what these are later), and cannot recursively contain
fields of their own type, although they can contain reference cells that
reference their own type.
Classes
Classes represent the “heart and soul” of OOP. Classes are types
that represent entities in the domain model. In OO systems, computation is
largely about creating instances of classes that contain state, i.e., objects, and
manipulating this state over time.
Please note that throughout this chapter, I will use the
term “type” and “class” synonymously. At their core, these two terms represent
the same construct and can be used interchangeable for all practical purposes.
Constructing Classes
Classes are user-defined types, introduced with the type
keyword and a primary constructor. The primary constructor is defined as
part of the type definition, and may or may not include parameters. The
following demonstrates the humble beginnings of an F# class:
type Movie()
This tells us we have a new type called Movie
and a primary constructor the takes 0 arguments ( ).
Following the primary constructor definition is an =
sign and the set of expressions that form the primary constructor’s body.
The body of the primary constructor can contain zero or more
let
bindings, which appear at the start of the type definition, before any member
definitions. Let’s take a look at a simple class definition that helps make all
of this a reality:
type
Movie() =
let Title = "Paranormal Activity"
This bit of code does a few things:
1. It
introduces a new type called Movie
2. It
introduces a primary constructor that takes no arguments: Movie().
The empty parens introduce the primary constructor.
3. It
defines a single let expression that forms the body of the
primary constructor.
A let expression in a
constructor implicitly defines a private
field or private function. This
means that the field Title
is private in the above example. Attributes and accessibility modifiers are not
allowed, therefore we cannot change the fact that Title
is private – it’s an inherent property of the let
binding as used in a constructor.
To make things a little more interesting, let’s
define a new Movie class that creates
2 private fields in its constructor (OK, it’s not that interesting, but
bear with me).
type
Movie() =
let Title = "Paranormal
Activity"
let Director = "Orin Peli"
In addition to executing these let expressions, constructors
can also execute arbitrary code using the keyword do followed by a series
of expressions. In the next example, we create a class that prints out a few messages
each time a new instance is created.
type
Movie() =
let Title = "Paranormal
Activity"
let Director = "Orin
Peli"
do
printfn "Now showing %s by %s"
Title Director
printfn "We hope you enjoy
the show!"
OK, so now we know how to specify a class,
create a primary constructor, and execute instance-specific code the form of let
and do expressions,
sometimes referred to as let
and do bindings.
To create a fresh instance of a class, use
the (optional) keyword new.
This causes a new instance to be created and initialized with the constructor,
e.g.:
let m = new Movie()
Note that using the keyword new
is optional. I tend to favor it because it’s explicit and conveys the purpose
of the call; however, if it suits your style, you can omit it without error,
e.g.:
let m = Movie().
Constructors
with Arguments
When we create class instances, we
generally want to pass in initialization information – this is, of course, the
whole point behind constructors. F# supports class constructors that accept
arguments, as shown in the following example:
type
Movie(t, d) =
let Title = t
let Director = d
do
printfn "Now showing %s by %s"
Title Director
printfn "We hope you enjoy
the show!"
Movie’s primary
constructor now requires 2 arguments – a title and a director. To create
instances of Movies we can execute
code similar to the following:
let ho =
new Movie("The
Hangover", "Todd Phillips")
let pa = new Movie("Paranormal Activity", "Orin Peli")
If F# has trouble determining the types of
the constructor’s arguments, you can add explicit type annotations to them. For
example, we can re-write the Movie type as follows, specifying the type of t
and d:
type
Movie(t: string, d: string) =
let Title = t
let Director = d
do
printfn "Now showing %s by %s"
Title Director
printfn "We hope you enjoy
the show!"
Constructors that accept arguments are flexible relative to
how these arguments work. First, the arguments of the primary constructor are
in scope throughout the entire class declaration. This means that you can use
the arguments passed into the primary constructor anywhere in the class. This
also means that you do not need to store the arguments - you can treat them as
implicit private fields.
Second, by default, constructor arguments bind to their
input by position, left to right; however, in F#, constructor arguments are
implicitly named, making it easy for callers to access them directly and/or out
of order, as in this next example:
type
Movie(title: string, boxOfficeSales: decimal) =
let netSales = boxOfficeSales * 0.90m // these numbers are made up
do printfn
"%s grossed %f at the box office, for
net sales of %f for the studio"
title boxOfficeSales netSales
let m = new
Movie(boxOfficeSales=100000000m, title="Jaws")
Notice here that when we call the Movie
constructor, we use the arguments’ names to map the values explicitly vs.
relying on their lexical ordering. We provide initial values using the arg-name=value
syntax.
Constructors with Optional Arguments
Constructor arguments can be made optional by placing a
question mark (?) before the parameter name. Optional
arguments must come at the end of the argument list. Under the covers, F# wraps
optional arguments in an Option (Some/None)
type, making them naturally compatible with pattern matching. In the following
example, we make the argument director optional:
type
Movie(title: string, ?director: string) =
let getDirector : string =
match director with
| Some(director) -> director
| None -> "NOT
SPECIFIED"
do printfn "title = %s, director = %s" title
getDirector
Typically, when processing optional arguments, we use
pattern matching to check for the presence (or absence) of a value.
When dealing with optional arguments, you often want to
provide default values, in case the caller chooses to skip passing values into
the function or method. To do this, you can use the function defaultArg.
The defaultArg
function takes the optional parameter as the first argument and the default
value as the second. In the following example, the ticketPrice parameter
is optional, and has a default value of 10.00.
type Movie(title: string,
?ticketPrice: float) =
let ticketPrice = defaultArg ticketPrice
10.00
do printfn "Welcome
to %s. Your ticket costs $%f" title ticketPrice
Overloaded Constructors
To provide different creation options its clients, a type
can offer more than one constructor. You can define overloaded constructors
using the following syntax:
new(arg-list) = constructor-body
In the following example, the Movie class contains 2
constructors: the primary constructor that accepts 2 parameters (string
and float),
and an overloaded constructor that takes 1 string.
type Movie(title: string,
ticketPrice: float) =
let Title = "["
+ title + "]"
do printfn "New
movie created: %s" Title
new(t: string) = Movie(t, 10.00)
You can define as many overloaded constructors as you want.
Just keep in mind that the body of the overloaded constructor must call the
primary constructor.
Note that the let and do
bindings defined in the primary constructor always execute, regardless of the
constructor called.
Running Arbitrary Code in Overloaded Constructors
A class’s primary constructor can execute arbitrary code via
a do
binding; however, an overloaded constructor cannot use this mechanism. In order
to execute arbitrary code in an overloaded constructor, we need to use the then
keyword, as shown in the following example:
type
Movie(title) =
do printfn "Welcome
to the movie: %s" title
new(t, d) =
Movie(t)
then printfn "Directed by %s" d
The then keyword allows us to run
constructor-specific code, after the let and do
expression from the primary constructor execute. The fact that the keyword is then
is a reminder of the order in which the let and do
constructs execute – those from the primary execute, then those from the
overload.
Self-Identifier
As part of the class definition, you can provide a
self-identifier. A self-identifier is the way the class references itself in
members (discussed shortly) such as properties and methods. In other words,
it’s the way an F# class represents “the current instance” of itself. In C++
and C#, the self-identifier has a special name – this. In F#, there is
no special name for the self-identifier – you can choose to call it whatever
you’d like.
You can define a self-identifier in one of two ways. If you
want the self-identifier to be available to the entire class, use the as
keyword as part of the type definition. If you want the self-identifier to be
available to a local member function, provide the name as part of the member
declaration.
In the following example, we embellish our Movie
class with a self-identifier. We will see how to use it when we discuss members
later in this chapter.
type
Movie() as this =
let Title = "Paranormal Activity"
Given the above declaration, the self-identifier this
enables us to refer to “this instance of the class” anywhere in the class. Note
that if the self-identifier is never used (as in the above example), the
compiler will issue a warning:
warning FS1183: The recursive object reference 'this' is
unused. The presence of a recursive object reference adds runtime
initialization checks to members in this and derived types. Consider removing
this recursive object reference.
So, only use as <self-identifier>
if you are going to use the self-identifier in subsequent constructs.
The Constructor Body – Details of let and do
As we’ve seen, the bodies of constructors
are made up of a series of let
and do expressions. Let’s
(no pun intended) take a closer look at their general syntax and capabilities.
let
In a class constructor, a let
expression takes the following form:
// Field
[static] let [ mutable ] binding
// Function
[static] let [ rec ] binding
A let expression can define a static or
non-static class member – either a field or a function. static
members are those that are defined on the class itself vs. on objects of the
class. As you can see from the above syntax, let
expressions can create mutable
and non-mutable fields, and can create recursive and non-recursive functions.
We contrast static let bindings with
instance let bindings. static let
bindings are guaranteed to execute at some point before the first instance of
the class is created. They only execute once for the lifetime of the class.
Up to this point, we’ve used let to define private
fields only. We can also use let to define private
functions. These functions are accessible only to the class itself. In the
following example, we use let to define a private function, calcAdmission
on the Movie class.
type
Movie() =
let Title = "Paranormal
Activity"
let calcAdmission numPeople = float numPeople *
10.25
do printfn
"If 4 people went to see %s, it would
cost $%5.2f" Title (calcAdmission 4)
do
F# class constructors can also include zero or more do
bindings, which enable the execution of arbitrary code when an instance of the
class is created. do bindings appear alongside let
bindings, and take the following form:
[static] do expression
static do bindings are guaranteed to execute
before the first instance of the class is created. They are only executed once
for the lifetime of the class. The combination of static let
and static
do
bindings form the “static constructor” of the class, and are run once, before
the first instance of the class is created.
Because let bindings initialize private fields
of the class, do bindings generally appear after them
lexically. This enables the do bindings to access the fields that
the let
bindings have just initialized.
Note that if your non-static do bindings need to
access members of the class to execute correctly, they can do so if the class
defines a self-identifier (see next section) and all access to the class’s
member are made via the self-identifier.
The net net of this binding story is that, together, the let
and do
bindings form the body of the primary constructor. The non-static let
and do
together form the instance constructor, while the static let
and do
together form the static constructor.
Alternate Field Creation Semantics
When you create class constructors, you can use an alternate
mechanism to create fields: the val keyword. To use the
val
keyword to create fields, we must adhere to the following rules:
·
use the [<DefaultValue>] attribute
·
mark the field as mutable
·
access the field through a self-identifier (this
in the example below).
In the following example, we demonstrate this technique
using the field director:
type
Movie() =
let title = "Jaws"
[<DefaultValue>] val mutable director: string
member this.SetDirector =
this.director <- "Spielberg"
The pragmatic difference between using val
vs. let
is that you can specifiy the access mode (public, internal,
private)
for a value created with val, whereas with let,
your fields are intrinsicly private and their accessibility cannot
be changed.
Adding Members to Classes
Classes would be of limited utility if they only contained constructors
and private fields. In practice, classes contain fields and functions (collectively
called members) that form their public interface. You can define members
on F# classes, structures, records, discriminated unions, and interfaces. We
ignored adding members to these constructs earlier in the text to avoid
confusion. After covering class members here, you will be in a position to add
them to these other types as well. We will show examples of adding members to
records, etc. as part of this chapter.
Members are declared using the keyword member. Collectively,
members represent a type’s “public face”; therefore, they have public
access unless declared otherwise.
The form a member takes depends on the nature of the member,
e.g., a property, method, etc. We will look at all of the possibilities in
turn.
Properties
Properties are members that represent values
associated with an object. They are like “smart fields” in that they have a
“getter” method and a “setter” methods, a.k.a., accessors, defined. The
full syntax for a property is as follows:
[ attributes ]
[ static ] member [accessibility-modifier] [self-identifier.]PropertyName
with [accessibility-modifier] get() =
get-function-body
and [accessibility-modifier] set parameter =
set-function-body
Properties may be declared static, in which case
they are defined on the type itself and are limited to accessing other static
members. We can also employ access specifiers to limit their visibility,
although they are public by default.
In this next example, we add a ReleaseDate property to
the Movie
class. This property exhibits both a get accessor for
reading the value and a set accessor for setting the value.
open
System // provides access to DateTime type
type
Movie(title: string, released: DateTime) =
let mutable
_releaseDate = released
member this.ReleaseDate
with get() =
_releaseDate
and set(newValue) =
_releaseDate <- newValue
As you can see here, we’ve defined a self-identifier called this via
the
as this following the type declaration.
There are a few things to note about this example:
·
T o implement a property, we generally create a private backing
field - a field that holds the value that the property manages. Here, our
backing field is called _releaseDate. By convention, I like to use an
underscore in my backing field definitions. F# imposes no such restrictions.
·
Properties that implement a set member use a mutable
backing; otherwise, callers won’t be able to change its value.
·
Properties are defined using a with and optional and
keyword. We will discuss the specifics of these shortly.
·
The setter receives a parameter that represents the new value of
the property.
To show how everything fits together, let’s look at the
following example:
open
System
type
Movie(title: string, released: DateTime) =
let mutable
_releaseDate = released
member this.ReleaseDate
with get() = _releaseDate
and set(newValue) = _releaseDate <- newValue
let mv =
new Movie("Paranormal
Activity", new DateTime(2010, 10,
1))
printfn
"The movie will be released %A"
mv.ReleaseDate
printfn
"Oops..."
mv.ReleaseDate
<- new DateTime(2009, 10, 1)
printfn "Correction...the movie will
be released %A" mv.ReleaseDate
As you can see in the example, we use the destructive
assignment operator (<-) to update the property value. Under the
covers, F# translates this to a call to the property’s set
function.
Properties are not required to have both getters and
setters. You can implement a read-only property by defining get
only, a write-only property by defining a set only. You can also
include access specifiers to limit the property’s availability, e.g., using internal
to limit access to those classes in the same assembly or module.
In the following example, we define a read-only property by
implementing a get only. We simply do not provide a set.
The syntax is quite straightforward, as shown here:
type
Movie(title: string, released: DateTime) =
let mutable
_releaseDate = released
member this.ReleaseDate // read only using a solo get()
with get() = _releaseDate
We can do the same thing to create a write-only property,
although this is rarely done in practice:
type
Movie(title: string, released: DateTime) =
let mutable
_releaseDate = released
member this.ReleaseDate
with set(newValue) =
_releaseDate <- newValue
Like other values, properties can be type-annotated via an
explicit type declaration. In the following example, the property TicketPrice
is declared as decimal via the getter member:
type
Movie(title: string, price : decimal) =
let mutable
_ticketPrice = price
member this.TicketPrice
with get() : decimal = _ticketPrice
and set(newValue) =
_ticketPrice <- newValue
Note that the get and set
functions are not considered methods per se. They are considered functions that
are used to implement property members. They are accessed indirectly via the
property itself and are not directly exposed outside of the property’s context.
In addition to classes, properties can be members of structures,
discriminated unions, records, interfaces, and type extensions (to be discussed
later) and can also be defined in object expressions (also to be discussed
later). In addition, properties can be declared abstract. We will cover
abstract
constructs later in this chapter, when we discuss inheritance.
Combining Static and Instance
Properties
There are several instances OOP when you want the class to keep track of the
instances with which it is associated, e.g., keeping a count of the number of
instances, limiting the creation of instances to a certain number or range,
etc. The following example shows how to accomplish this in F# via static
members. We’ve updated the Movie class to keep track of the number
of Movies
currently playing in theaters:
type
Movie(title: string) =
static let mutable numInTheaters = 0
do numInTheaters <- numInTheaters + 1
do printfn "Current
count of movies in theaters = %d" numInTheaters
let m1 =
new Movie("Paranormal
Activity")
let m2 =
new Movie("The
Surrogates")
let m3 = new Movie("Toy Story 3")
Recall that static let and static do
bindings are executed once per type, while the non-static bindings are
executed each time an instance is created. The results of running
the code are:
Current count of movies in theaters = 1
Current count of movies in theaters = 2
Current count of movies in theaters = 3
Indexed Properties
There are times when you want to store collections of items
in a property, and provide indexed access to the information. F# supports indexed
properties just for this purpose. They take the following form:
member self-identifier.PropertyName
with get(index-variable) =
get-function-body
and set index-variables value-variables =
set-function-body
As you can see, an indexed property takes an index variable
as a parameter. It’s the property’s job to map the index to an appropriate
piece of data. To demonstrate how indexed properties work, let’s add a list of
actors to our Movie class:
type
Movie(title, actors: string[]) =
do printf "%s..."
title
let mutable
_actors = actors
member this.Actors
with get(index) =
if index < _actors.Length then _actors.[index]
else "Unknown
actor. Check the index."
and set index value =
if index < _actors.Length then _actors.[index] <- value
let m = new Movie("The
Hangover", [| "Bradley Cooper";
"Ed Helms";
"Zach Galifianakis"; "Justin Bartha" |])
let star
= m.Actors(0)
printfn "the star of the movie is
%s" star
Indexed Properties with Item
Now, although the above example works, it really doesn’t
capture the spirit of indexed properties. The client is still calling accessing
a distinct property and passing an index. If we want to make the object instance
look like a collection of things, we can use the specially-named property Item.
When we do this with indexed properties, F# provides some syntactic sugar,
enabling us to access the instance as if it were a collection.
Here is a new version of the Movie class that uses the
specially-named Item property. I have removed the error
checking to simplify the example:
type
Movie(title: string, actors: string[]) =
let mutable
_actors = actors
member this.Item //
<<< specially-named property
with get(index) = _actors.[index]
and set index value = _actors.[index] <-
value
let m = new Movie("The
Hangover", [| "Bradley Cooper";
"Ed Helms";
"Zach Galifianakis"; "Justin Bartha" |])
let star
= m.[0] // <<< now we can use the
instance like a collection
printfn "The star of the movie is
%s" star
When we access m.[0], behind the
scenes, F# translates this into a call to m.Item.get(0). This
syntax sugaring enables clients to treat instances like indexed collections.
Indexed Properties with Multiple Indices
Indexed properties can have more than one index variable.
This is useful when dealing with 2D arrays and other matrix-like structures.
For example, if we build a 2D array that contains a multiplication table (think
back to 4th or 5th grade), we can access the product of
two numbers simply by accessing the intersection of the row and column
specified by the numbers we’re multiplying. We can combine this with the
specially-named property Item as well, as in the following
example:
type
MTable() =
let _table = Array2D.zeroCreate<int> 12
12
do for x = 1 to 12 do for y = 1 to 12 do _table.[x - 1, y - 1] <- x * y
member this.Item
with get(a, b) = _table.[a - 1, b - 1]
let m = new MTable()
let product = m.[3, 7]
From the F# Interactive Console: val product : int = 21
Methods
A method is a function, a.k.a., a behavior, that is
associated with a class. In OOP, methods are the primary mechanism used to
expose functionality and behaviors of objects and types. Methods may be
instance methods, virtual instance methods, static methods, or abstract methods.
In this section, we will cover static and instance methods. We will cover
virtual and abstract methods once we’ve covered the F# inheritance model.
Static methods
Static methods are those that are defined using the keyword static,
which binds the method to the type itself vs. instances of the type. In other
words, all instances of the type share the type’s static method. Static
methods can access static members, and are callable only through
the type itself. To define a static method, we use
the following syntax:
[ attributes ]
static member [inline] method-name parameter-list [ : return-type ] =
method-body
static methods are useful when proper execution
is independent of the state of a given instance, i.e., when instance state does
not affect the outcome. Many mathematical members, e.g., square root functions,
etc., are implemented like this. In the follow example, let’s write a DistanceConverter
type that converts between miles and kilometers to illustrate the idea:
// 1 kilometer = 0.621371192 miles, 1 miles = 1.609344 kilometers
type
DistanceConverter() =
static member
KilometersToMiles k: float = k * 0.621
static member
MilesToKilometers m: float = m * 1.609
let x = DistanceConverter.KilometersToMiles
10.0
let y = DistanceConverter.MilesToKilometers 8.0
Notice that static methods do not
include a self-identifer – we use member KilometersToMiles
instead of member this.KilometersToMiles.
static
members, by their nature, do not accept self-identifiers, since they are not
associated with any instance. In contrast to static methods, we have
instance methods, discussed next.
Instance Methods
Instance methods are those that are associated with each instance
of the class. Instance methods take into account object state, and may change
it during the course of execution. They are the “standard” methods for objects.
In F#, we define instance methods using the following syntax:
[ attributes ]
member [inline] self-identifier.method-name parameter-list [ : return-type ]=
method-body
The two main differences in this syntax vs. static
methods are: the absence of the static keyword and the
introduction of a self-identifier. Let’s create an updated Movie
class that implements a single instance method, CalcTicketPrice.
type
Movie(title: string) =
member this.CalcTicketPrice =
if title.ToLower() = "jaws"
then 7.50 // old
movie
else 10.25 // more
like today's prices
let m1,
m2 = new Movie("The
Hangover"), new Movie("Jaws")
let p1, p2 = m1.CalcTicketPrice, m2.CalcTicketPrice
Overloaded Methods
F# types also support overloaded methods. A type defines overloaded
methods when it declares two or more methods with the same name but taking a different
number of arguments or the same number of arguments of differing types.
In F# we tend to use optional arguments instead of
overloaded methods; however, overload methods can provide a convenient way to
create methods that take completely different arguments. Let’s look at a few
examples:
type
Movie(title: string, director: string, zip: string) =
member this.Find(t, d) =
if title = t && director = d then printfn "Movie
found by title and director"
else printfn "Cannot
find movie by title and director"
member this.Find(z) =
if zip = z then
printfn "Movie found by zip code"
else printfn "Cannot
find movie by zip code"
let m = new Movie("Paranormal
Activity", "Oren Peli",
"01776")
m.Find("Paranormal Activity", "Joe Smith")
m.Find("Paranormal Activity", "Oren Peli")
m.Find("01776")
Overloaded methods can have the same number of parameters,
so long as at least one is of a different type; however, due to the fact that
F# can curry functions, we generally want to specify our argument list in
tupled form:
// Define a class with overloaded methods
type Foo() =
member this.bar(x: int) = ()
member this.bar(x: string) = ()
member this.bar x = ()
member this.bar(x: int, y: string) = ()
member this.bar(x: int, y: float) = ()
// Call overloaded methods
let f = new Foo()
f.bar(1)
f.bar("s")
f.bar
100M
f.bar(100,
"s")
f.bar(200,
2.34)
f.bar
true
You can define as many overloaded methods on your classes as
you need to.
Events
An event is a messages that one object sends to one or more
other objects. An event signals that “something happened.” When an object sends
an events, we say that the object “fired the event” or “raised the event.” The
object that fires the event is called the event sender or the event
source. The object receiving the event is called the event target, event
sink or listener. Listeners implement event handler functions
that are called in response to an event being fired.
Events are characterized as distinct from properties and
methods in that clients do not call event members. Instead, clients register
interest in events that might eventually be raised by an event sender. With
event communication, senders are disconnected from listeners. The sender does
not know which object or method will receive the events it raises. To
accommodate this “publish and subscribe” model of programming, there needs to
be a broker between the sender and its receiver(s). Under the covers, .NET
uses the delegate mechanism for this purpose.
Creating Delegates to Be Used with Events
For the purposes of discussing events, we can briefly
describe delegates here.
In F#, we use the following syntax to define delegates:
type delegate-type = delegate of type -> type
The delegate type you create is designed to wrap, i.e.,
call, a function of the same type. For example, if we have a function that
accepts an integer value and returns a float, e.g.:
let f n
=
float n / 2.0
We can create a delegate type that can call this function:
type FDelegateType = delegate
of int ->
float
To call a function through a delegate, we need to create a
new instance of the delegate and “wrap” the target function, e.g.:
let fd = new
FDelegateType(f)
To call the function through the delegate, we use the
delegate’s Invoke method:
let result = fd.Invoke(10)
Of course, we can use delegates to wrap lambda functions as
well. We could have written the above example as:
type
FDelegateType = delegate of int -> float
let fd =
new FDelegateType(fun
n -> float n / 2.0)
let result = fd.Invoke(10)
Creating Events
To create an event, you use the new Event<'delegate,
'argument>
construct. This creates and returns an event object that senders can
subsequently fire. The first parameter to the Event constructor is a
delegate type that defines the listener function. The second parameter is the
type of argument the event sends along when it’s fired, i.e., information attached
to the event sent to the listener function.
When creating delegates to be used with events, we’re
expected to follow certain conventions. First, our event delegates are expected
to take two parameters – the source, i.e., the object raising the event, which
must be specified as type obj, and the event argument. The event
argument can be of any type; however, it’s mostly either a simple type, e.g.,
string, or an object derived from System.EventArgs. The
event delegate is expected to return unit. In the following
example, we create a StockAlerter class that implements an event
indicating when a given stock should be sold.
The first thing we’ll do is create the delegate type, based
on the rules defined above:
// Event delegate. Follows expected delegate conventions: obj *
args -> unit.
type StockAlertDelegate = delegate
of obj * string ->
unit
This delegate type tells us that the listener function accepts
two parameters: the sender, which must be specified as type obj,
and the event argument. Here, we are choosing to pass a simple string. We can
use this delegate and argument to implement create an appropriate Event
instance in the StockAlerter class:
// Event source. Raises StockAlert events.
type
StockAlerter(symbol: string) =
do printfn "Watching
%s" symbol
let alertEvent = new Event<StockAlertDelegate,
StockAlertEventArgs>()
After defining an event, we need a way for clients, a.k.a.
subscribers, to register interest in the event. To make our events visible to
clients and eligible for subscription, we need to publish the event. We
do this via a public member of the class and
the Publish
method of the event class:
member this.StockAlertEvent = alertEvent.Publish
Clients access the alert event via the public member StockAlertEvent.
The only thing missing is a way to actually fire the event. Generally speaking,
the event source implements some internal algorithm that results in it firing
an event, e.g., it might check the value of a stock every five minutes and fire
an event if the price changes by some specified percentage. For our purposes,
we’ll create a function that fires the event whenever it’s called. The entire StockAlerter
class is shown below:
// Event source. Raises StockAlert events.
type
StockAlerter(symbol: string) =
do printfn "Watching
%s" symbol
let alertEvent = new
Event<StockAlertDelegate, string>()
member this.StockAlertEvent =
alertEvent.Publish
// Fire event. this=sender=StockAlerter instance,
formatted string is argument
// passed to client handler function.
member this.SellStock() = alertEvent.Trigger(this, sprintf
"sell %s!" symbol)
When we define events that need to carry extra information
to their handlers, we implement a class that derives from System.EventArgs,
e.g.:
// Event argument. Carries information about the event.
type
StockAlertEventArgs() =
inherit System.EventArgs()
member this.message = "Time
to sell!"
We then use the class type as the second parameter
to the delegate definition:
// Event delegate. Follows expected delegate convention: obj *
args -> unit.
type
StockAlertDelegate = delegate of obj * StockAlertEventArgs ->
unit
Whenever our code fires an event, it needs to pass along the
sender and an instance of the appropriate event argument.
Listening for Events
Events are useless unless something is listening for them to
occur. To continue our stock alerting example, let’s create a StockBroker
class that implements an event listener:
// Event sink.
type
StockBroker() =
// Create a StockAlerter
let sa = new
StockAlerter("MSFT")
// Create an event listener via creating the
appropriate delegate
let eventListener = new
StockAlertDelegate(
fun sender msg ->
printfn "%A tells us to: %s" sender
msg)
do
// Add listener to event's list of listeners
sa.StockAlertEvent.AddHandler(eventListener)
// Cause event to fire. Normally, the StockAlerter
would fire its
// own event independtly. We're doing this here to
demonstrate
// the mechanism.
sa.SellStock()
To add a listener function, we access the source event,
e.g., sa.StockAlertEvent, and call it’s AddHandler
method. AddHandler adds the given delegate to the event’s internal
list of listeners. When the event fires, the underlying event class iterates
over its list of listeners and calls each one in turn via the stored delegate.
When a client no longer cares to receive events from a
source, it need to unregister its interest in the given event. To unregister interest
in an event, the client can call the event’s RemoveHandler method,
as shown:
type
StockBroker() =
// …
// Unregister
sa.StockAlertEvent.RemoveHandler(eventListener)
// Nothing printed now since we’re not listening
anymore
sa.SellStock()
Since the Event class maintains
an internal list of delegates to the client handler functions, we want to
unregister handlers whenever the client handlers go out of scope, or whenever
we no longer need an active event connection. If we are not careful about
disconnecting our event listeners from our event sources, we can end up
crashing the application, since the event source may try to call a listener
function that no longer exists.
Observable Events
As of the latest release, F# events are first-class citizens
that implement the IObservable interface. From Don
Syme’s blog on the CTP 1.9.7.8 release:
The IObservable and IObserver interfaces provide a
general interface over observable data, which can be easily composed.
These interfaces are provided in .NET4.0, and are available in F# for
.NET2.0.
1st class events in F# now implement the IObservable
interface, enabling them to interoperate with other IObservable programming
models. A new Observable module in the F# library provides compositional
functions for manipulating general Observable data:
Observable.merge
Observable.map
Observable.filter
Observable.partition
Observable.split
Observable.choose
Observable.scan
Observable.add
Observable.subscribe
What this means to us is that we can use the Observable
module to work with events in a functional manner. For example, let’s suppose
we want to limit the stock events we care about to those whose associated stock
symbols are 3 characters exactly. We can use the Observable.filter
method to do just this:
// Event delegate.
type
StockAlertDelegate = delegate of obj * string ->
unit
// Event source. Raises StockAlert events.
type
StockAlerter(symbol: string) =
do printfn "Watching
%s" symbol
let alertEvent = new
Event<StockAlertDelegate, string>()
member this.SellStock() =
alertEvent.Trigger(this, symbol)
[<CLIEvent>] // <<< .NET and
Observable friendly
member this.StockAlertEvent =
alertEvent.Publish
// Event sink.
type
StockBroker(sa: StockAlerter) =
// Create an event listener via creating the
appropriate delegate
let eventListener = new
StockAlertDelegate(
fun sender msg ->
printfn "%A tells us to: %s" sender
msg)
let filteredEvent =
sa.StockAlertEvent
|> Observable.filter(fun (sym: string) -> sym.Length = 3)
// only watch symbols with 3 letters
|> Observable.add(fun msg -> printfn "Filtered
event: SELL %s" msg)
// hook up event handler
do
sa.SellStock()
// We expect to see an IBM message. We do not expect to see a MSFT
message.
let
saIBM, saMSFT = new StockAlerter("IBM"), new
StockAlerter("MSFT")
let
sbIBM, sbMSFT = new StockBroker(saIBM), new StockBroker(saMSFT)
In order to work with events via the Observable
module, the events must be marked with the [<CLIEvent>]
attribute, making them .NET-friendly. Additionally, to hook up a listener to an
Observable,
use the Observable.add method, as
demonstrated above. Please consult the MSDN
documentation for the fully skinny on Observable.
ignore
To be honest, I wasn’t sure where to discuss the ignore
keyword, so I thought it would be somewhat appropriate here, in the section in
events and event processing. To tell F# that you want to ignore the result of
an expression and always return unit, you can use ignore.
This can be helpful to ignore events (thus its introduction
here), return unit where the normal flow of code returns a non-unit, etc. Here
are some examples of using ignore:
let test
= ignore 10 // test = unit
System.Console.ReadKey() |> ignore // return
type is now in line with expectations
Use ignore when you want to return unit
wherever it’s expected. For example, if you fail to use |>
ignore in the example above, F# will warn you that “The
expression should have type 'unit', but has
type 'System.ConsoleKeyInfo'.”
Specifying Generic Class Types
When defining a class, you can specify that it accepts one
or more type parameters, much like the built-in collection classes do. For
example, if we wan to implement a class that compares two objects of the same
type, we could specify the class as follows:
type
MyComparer<'a>() =
member this.Compare(x: 'a, y: 'a, f) =
f x y
As you can see, we specify generic type parameters in angle
brackets(<>) as part of the type declaration. In this example,
using generics helps to express and enforce the intent of comparing only
objects of the same type. In this implementation, we supply the elements to
compare as well as a comparison function that is subsequently applied.
Note that there is an alterate syntax for specifying generic
parameters, where the generic parameters are specified before the type name. To
rewrite the MyComparer type using this alternate syntax, we
would write it as follows:
type 'a
MyComparer() =
member this.Compare(x: 'a, y: 'a, f) =
f x y
In general, I prefer the angle-bracket syntax; however, you
will see both in this book and in other F# code.
Wildcard as Type Argument
When building a generic type or function, you can instruct
the compiler to infer the type argument by using the wildcard symbol (_),
instead of a named type argument. This is shown in the following code.
let
printSequence (s: seq<_>) =
Seq.iter (fun elem -> printf "%s
" (elem.ToString())) s
This form is generally reserved for cases when the code does
not reference the type argument, as in this example. We could just as easily
have written the example this way:
let
printSequence2 (s: seq<'a>) =
Seq.iter (fun elem -> printf "%s
" (elem.ToString())) s
And it would have worked identically.
Generic Type Constraints
When dealing with generic types, there are times when you
may want to restrict or constrain the types that can be used. F# supports the
following flavors of constraints: type constraints, null constraints, explicit
member constraints, constructor constraints, value type constraints, reference
type constraints, enumeration type constraints and delegate constraints. These
are summarized in the MSDN
documentation found here.
We need to use genetic type constraints when our code requires
a feature that is available on the constraied type but is not guaranteed to be
available on a generic type. For example, if our code depends on the generic type
to implement a particular interface, e.g.., IComparable, we can
constain the generic type argument to accept only those classes that
implement the IComparable interface. Let’s take a look at a
few of the most often used constraints and provide an example of their use.
Type Constraints
Type constraints are the most often used of the constraint family. A type constraint
limits the provided generic arguments to be equal to or derived from the type
specified. It is also used to constrain the provided type argument to a type
that implement a given interface. In the following example, the generic type argument
is limited to those types that implement the ILoveProgramming
interface (we discuss interfaces later in this chapter):
type
ILoveProgramming =
abstract Program: unit ->
unit
type
Programmer<'a when 'a :>
ILoveProgramming>() =
do printfn "I am a programmer who loves to program!"
Note the use of the symbol ( :> ). Among other
things, it is used in the type constraint syntax to specify the type to which
the generic must conform.
For fine-granied control over generic type arguments, we can
combine constraints. In the following example, we can only construct instances
of the MyCollection via by supplying arguments that are Stacks
(found in System.Collections) implementing the IDisposable
interface:
open
System
open
System.Collections
type
MyCollection<'a when 'a :> IDisposable and 'a :> Stack>() =
do printfn "We only accept disposable stacks here, my
friend."
Of course, if your type accepts multple type arguments, you
can constrain each of them individually, as in the folllowing example:
open
System.Collections
open
System.Collections.Generic
type
MyCollection<'a, 'b when 'a :>
IEnumerable<'a> and 'b :>
ArrayList>() =
do printfn "'a must implement IEnumerable, 'b must be an
ArrayList of some sort."
Constructor Constraints
The constructor constraint is used to ensure that the
provided type has a public, default constructor. A default constructor is one
that takes no arguments. I guess a more accurate name for this constraint is
the “public default constructor constraint.”
It’s possible that a .NET class not implement a a
public, default constructor. This happens when the class author has implemented
a public constructor that takes parameters. In these cases, the compiler does
not generate a default constructor.
The constructor constraint provdes useful when creating
collections of objects, where the absence of a default constructor can cause
problems. In the following example, the generic argument that we use to create
MyCollection must implement a public, default constructor:
type
MyCollection<'a when 'a: (new : unit ->
'a)>() =
do printfn "default constructor constraint applied"
The other type constraints adhere a similar syntax and usage
pattern.
Mutually Recursive Class Definitions
If you need to define types that reference each other and
create a circular dependency, you need to string together the type definitions
by using the and keyword. The and keyword replaces
the type keyword on all except the first definition, as shown here:
type [access-modifier] type-name1 ...
and [access-modifier] type-name2 ...
In the following example, we define two classes, Student
and University,
each needing to know about the other.
type
Student(ssn: string, u: University) =
let _ssn, _u = ssn, u
and
University() =
member this.EnrollStudent(s:
Student) = ()
General Form
As a summary, here is the general from of a class:
type [access-modifier] type-name [type-params](
parameter-list ) [ as identifier ] =
[ class ]
[ inherit base-type-name(base-constructor-args) ]
[ let-bindings ]
[ do-bindings ]
member-list
...
[ end ]
Here is a quick run-down on the class declaration’s
piece-parts:
·
type – keyword that introduces a new type.
We’ve seen this before with records, etc.
·
access-modifier – public, internal
or private.
·
type-name – the class name.
·
type-params – used when you want to add generic
type parameters to be used in the class, e.g., <'a>.
·
parameter-list – used to create instances of
the class (what you pass to the constructor).
·
class – optional keyword introducing the class
body. Pairs up with the optional end keyword.
·
inherit – used to specific this class’s base
class, if any.
·
let-bindings - enables us to declare fields
and function values that are local to the class.
·
do-bindings – includes code to be executed when
the an object of the class is constructed.
·
member-list – contains constructors, static
method declarations , instance method declarations, interface declarations,
abstract bindings, and property and event declarations.
·
as identifier – provides a local alias that the
class itself can use in its definition – a “self identifier” akin to the this
keyword in C++ and C#.
·
end – optional keyword that marks the end of
the class body. Pairs up with the optional class keyword.
Adding Constructors and Member Functions to Aggregate Types
Now that we’ve discussed classes and their component parts,
let’s briefly revisit the aggregate types and demonstrate how to add members,
e.g., properties and methods, to them.
Structures
As discussed earlier in this chapter, structures, like
classes, can have a primary constructor, and you can provide additional
constructors by using keyword new. Note that
structures always have a default constructor (that is, one with no arguments)
generated by F#. The default constructor initializes all the fields to the
default value for that type, zero for numeric types and null for object types. Any
constructors that you define for structures must have at least one argument so
that they do not conflict with the default constructor that F# generates for
you. Structures can also include members such a properties, methods and events,
as shown below (sans events, for brevity).
[<Struct>]
type
Movie =
val Title: string
val Director: string
val ReleaseYear: int
new(t, d, y ) = { Title = t; Director = d;
ReleaseYear = y}
member this.Marquee
with get() =
"*** " + this.Title + " by " + this.Director + " released " + string
this.ReleaseYear + " ***"
member this.CalcTicketPrice() =
if this.ReleaseYear >= 2009 then 11.00 else 9.00
let mv =
new Movie("Where
the Wild Things Are", "Jonze",
2009)
printfn
"%s" mv.Marquee
let p =
mv.CalcTicketPrice()
printfn "That'll be $%2.2f,
please!" p
Discriminated Unions
Although discriminated unions do not support constructors
per se, individual discriminators can accept initialization parameters. In
addition discriminated unions can include other members, e.g., properties,
methods, and events. In the following example, we define a discriminated union Shape
that includes a method Draw:
type
Shape =
| Circle of double
| Triangle of double * double * double
| Square of double
member this.Draw() =
match this with
| Circle(x) -> printfn "Drawing circle of radius %f" x
| Triangle(x,y,z) -> printfn "Drawing triangle with sides %f %f %f" x
y z
| Square(x) -> printfn "Drawing a square with diagonal %f" x
let c, t
= Circle(10.0), Triangle(3.0, 4.0, 5.0)
c.Draw(), t.Draw()
Records
Similar to discriminated unions, records do
not support constructors explicitly, but employ the creation semantics as
discussed previously. Records also support properties and methods. Records are
different from classes in that their fields are automatically exposed as
properties. Let’s define a Student record that demonstrates these
capabilities:
type
Student =
{
Name: string;
YearEntered: int;
mutable GPA: float;
}
member this.GraduationYear
with get() = this.YearEntered + 4
member this.UpdateGPA(newGPA: float) =
this.GPA <- newGPA
let s =
{ Name="Jim Kress";
YearEntered=2008; GPA=3.1 }
s.UpdateGPA
3.8
printfn "%s will graduate in %d with
a %f GPA" s.Name s.GraduationYear s.GPA
When to Use Classes, Unions, Records, and Structures
Given the variety of types to choose from, it can be hard to
know which ones to address a given problem or scenario. The following rules of
thumb are adapted from the MSDN
documentation:
·
Classes are the lingua franca of OOP, and are used heavily
throughout the .NET framework. If your application is object-oriented, or needs
to interact closely with other .NET components written in C#, VB. NET, etc.,
classes are a good choice.
·
If you are not interoperating closely with OO code, you should
consider using records and discriminated unions. A discriminated union,
together with appropriate pattern matching code, can often be used as a simpler
alternative to an object hierarchy.
·
Records have the advantage of being simpler than classes, but
records are not appropriate when the demands of a type exceed what can be
accomplished with their simplicity. Records are basically simple aggregates of
values that can perform custom actions. They do not contain private fields, and
cannot participate in inheritance or interface implementations. Although
members such as properties and methods can be added to records to make their
behavior more complex, the fields stored in a record are still a simple
aggregate of values.
·
Structures are also useful for small aggregates of data, but they
differ from classes and records in that they are .NET value types. Classes and
records are .NET reference types. Therefore, structures are appropriate for
frequently accessed data when the overhead of accessing the heap is a concern.
What You Need to Know
·
F# supports a full complement of OO technologies include
structures, classes, interfaces, inheritance, etc.
·
Structures in F# are stack-based value types that enable you to
logically and semantically group related data. Structures can have implicit or
explicit constructors, immutable fields, and mutable fields.
·
You use the [<Struct>] attribute to declare a
structure.
·
To create fields, you use the val keyword.
·
You create instances of structures using the new
keyword.
·
Fields marked as mutable are modified
via a mutable structure instance.
·
F# supports the access specifiers public, internal
and private.
protected
is listed as a reserved keyword for the future.
·
F# supports classes. You use the type keyword to define
them.
·
Classes support the standard OO structures of constructors,
overloaded constructors, properties, methods, events, interfaces, statics, etc.
·
F# does not have a specific keyword for identifying object
instances – you can define your own. Many people tend to use this based on past
experience.
·
You create instances of classes via the new
keyword (new is optional). Many people tend to use new because it’s explicit
and clear.
·
You can use generic parameters when defining classes. You can
specify them in one of two ways: type 'a MyClass()
or type MyClass<'a>.
·
Generic type can be constrained, e.g., to being inherited from a
given ancestry using the :> operator.
·
F# supports flexible types, which constrain a type to a given
hierarchy. The syntax is #type. You see flexible types used often
in conjunction with the Seq type to accommodate different IEnumerables.
·
In addition to classes, you can add constructors and members to
aggregate types such as structures, unions and records.
|