Sie sind auf Seite 1von 20

CHAPTER 6

Type Definitions, Classes and


Objects
Chapters 2 to 5 have dealt with the basic constructs of F# functional and imperative
programming, and by now we trust you feel familiar with the foundational concepts and
techniques of practical, small-scale F# programming. In this chapter we cover additional
constructs related to defining types and object-oriented programming. Initially we focus on
describing the constructs themselves, and toward the end of the chapter we discuss
techniques for deploying and combining these constructs effectively in combination with the
core language features we’ve already encountered.

Defining Type Abbreviations


Type abbreviations are the very simplest type definitions:
type index = int
type flags = int64
type results = string * TimeSpan * int * int
It is common practice to use lower-case names for type abbreviations. Type abbreviations
may be generic:
type StringDictionary<'a> = System.Collections.Generic.Dictionary<string,'a>

type ('a,'b) maps = ('a -> 'b) * ('b -> 'a)


Type abbreviations are not “concrete”, as they simply alias an existing type. Type
abbreviations are expanded during the process of compiling F# code to the format shared
between multiple .NET languages. The difference can, for example, be detected by other
.NET languages, and because of this a number of restrictions apply to type abbreviations. For
example, you cannot augment type abbreviations with additional members, as can be done
for concrete types such as records, discriminated unions and classes. In addition, you cannot
truly hide a type abbreviation using a signature (again see Chapter 7).

Defining Records
The simplest concrete type definitions are records. Here’s our first example:
type Person =
{ Name: string;
DateOfBirth: System.DateTime; }
Record values may be constructed simply by using the record labels:
> let bill = { Name = "Bill"; DateOfBirth = new System.DateTime(1962,09,02) }
val bill : Person = { Name="Bill"; DateOfBirth = 02/09/1962 }

Records values may also be constructed by using the following more explicit syntax, which
names the type should there be a conflict between labels amongst multiple records:

> let anna = { new Person


with Name = "Anna"
and DateOfBirth = new System.DateTime(1968,07,23) }
val anna : Person = { Name="Anna"; DateOfBirth = 23/07/1968 }

Record values are often used to return results from functions:


type PageStats =
{ Site: string;
Time: System.TimeSpan;
Length: int;
NumWords: int;
NumHRefs: int }
This technique works well when returning a large number of heterogeneous results.
let stats site =
let url = "http://" + site
let html,t = time (fun () -> http url)
let hwords = html |> getWords
let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
{ Site=site; Time=t; Length=html.Length; NumHRefs=hrefs.Length }

Here is the type of stats and how F# Interactive shows the results of applying the function:

val stats : string -> PageStats


> stats "www.live.com";;
{ Site="www.live.com"; Time=0.872623628; Length=7728, NumWords=1156;
NumHRefs=10; }

Handling Non-unique Record Field Names


Records labels need not be unique amongst multiple record types. Here is an example:
type Person =
{ Name: string;
DateOfBirth: System.DateTime; }
type Company =
{ Name: string;
Address: string; }
When record names are non-unique, constructions of record values may need to use object
expressions in order to indicate the name of the record type, thus disambiguating the
construction. For example, consider the following type definitions:
type Dot = { X: int; Y: int }
type Point = { X: float; Y: float }
On lookup, record labels are accessed using the “.” notation in the same way as properties.
One slight difference is that in the absence of further qualifying informataion; the type of the
object being accessed is inferred from the record label. This is based on that latest set of
record labels in scope from record definitions and uses of open. For example, given the
above definitions we have:

> let coords1 (p:Point) = (p.X,p.Y)


val coords1 : Point -> int * int
> let coords2 (d:Dot) = (d.X,d.Y)
val coords2 : Dot -> float * float
> let dist p = sqrt (p.X * p.X + p.Y * p.Y) // use of X and Y implies type “Point”
val dist : Point -> float

The accesses to the labels X and Y in the first two definitions have been resolved using the
type information provided by the type annotations. The accesses in the third definition have
been resolved using the default intepretation of record field labels in the absence of any other
qualifying information.

Using Mutable Record Fields


Record fields may be labelled mutable. This is usually done for records that implement the
internal state of objects, a topic we’ll discuss in the next chapter. This means that record
fields can be updated using the <- operator, i.e. the same syntax used to set a property.
type ConnectionStats =
{ mutable TotalLength: int;
mutable NumPages: int; }

let fetch cstats url =


let page = http url
cstats.NumPages <- cstats. NumPages + 1
cstats.TotalLength <- cstats.TotalLength + page.Length
page

The function fetch takes two arguments: one a mutable “tracker” record used to accumulate
statistics and the other the URL to access. Programming with mutable data structures is
covered in more detail in Chapter TECHNIQUES.

> let cstats = { TotalLength = 0; NumPages = 0; }


val it : ConnectionStats = { TotalLength = 0; NumPages = 0 }

> ( fetch cstats "http://www.smh.com.au";


fetch cstats "http://www.theage.com.au ";
cstats);;
val it : ConnectionStats = { TotalLength = 8372; NumPages = 2 }
Finally, record types may also support members (e.g. properties and methods) and give
implicit implementations of interfaces, as discussed further below. This makes them a very
powerful device for implementing object-oriented abstractions.

Cloning Records
Records support a convenient syntax to clone all the values in the record, creating a new
value, with some values replaced. Here is a simple example:
type Point3D = { X: float; Y: float ; Z : float }
let p1 = { X=3.0 ; Y=4.0 ; Z=5.0 }

> let p2 = { p1 with Y=0.0; Z=0.0 }


val p2 : Point3D = { X=3.0 ;Y=0.0 ;Z=0.0 }

The definition of p2 is identical to:


let p2 = { X=p1.X; Y=p2.Y; Z=0.0 }
This expression form does not mutate the values of a record, even if the fields of the original
record are mutable.

Using Records as Abstract Values


Records can be used to model simple abstract entities simply by making the fields of the
record type function values:
open System.Drawing

type Shape =
{ Scale: float -> unit;
Draw: Graphics -> unit }
Record values are implemented using record expressions:
let Rect(top,left,bottom,right) =
let state = ref (new Rectangle(top,left,bottom,right))
let scale{n} =
let r = !state in
state := new Rectangle(truncate r.Top,...)
let draw(g:Graphics) = ...
{ Scale=scale;
Draw=draw }

let Circle(top,left,bottom,right) =
let state = ref (new Rectangle(top,left,bottom,right))
let scale{n} =
let r = !state in
state := new Rectangle(truncate r.Top,...)
let draw(g:Graphics) = ...
{ Scale=scale;
Draw=draw }
let r1 = Rect(...)
let r2 = Circle (...)
let f = new Form()
let t = new Timer()
t.Interval <- 10
f.Paint.Add(fun evArgs -> ...)
The above is an example of using records to define “abstract values”. Abstract values are
simply values that might in theory have many different possible underlying implementations,
which means that both the code and data associated with a value is not immediately known
from the static type of the value. We’ve already seen several examples of this, e.g. function
values of types such as int -> int are a very simple kind of abstract value. Well-designed
abstract types are often compositional.

Defining Discriminated Unions


The second kind of concrete type definition is a discriminated union. Here is a very simple
example:
type Route = int
type Make = string
type Model = string
type Transport =
| Car of Make * Model
| Bicycle
| Bus of Route
Each alternative of a discriminated union is called a “constructor”. Values can be built simply
by using the constructor much as if it were a function:

> let nick = Car("BMW","360");;


val nick : Transport
> let don = [ Bicycle; Bus 8 ];;
val don : Transport list
> let james = [ Car "Ford Fiesta"; Bicycle ];;
val james : Transport list

Constructors can also be used in pattern matching:


let averageSpeed (tr: Transport) =
match tr with
| Car _ -> 35
| Bicycle -> 16
| Bus _ -> 24

Several of the types we’ve already met are defined as discriminated unions. For example, the
'a option type is defined as follows:

type 'a option =


| None
| Some of 'a
Discriminated unions may include recursive references (the same is true of records and other
type definitions). This is frequently used when representing structured languages via
discriminated unions:
type Proposition =
| True
| And of Proposition * Proposition
| Or of Proposition * Proposition
| Not of Proposition

Recursive functions are required to define the semantics of this kind of type. For example:
let rec eval (p: Proposition) =
match p with
| True -> true
| And(p1,p2) -> eval p1 && eval p2
| Or (p1,p2) -> eval p1 or eval p2
| Not(p1) -> not (eval p1)

Indeed, the F# type of immutable lists is defined in this way:


type 'a list =
| ([])
| (::) of 'a * 'a list
A broad range of tree-like data structures are very conveniently represented as discriminated
unions. For example:
type Tree<'a> =
| Tree of 'a * Tree<'a> * Tree<'a>
| Tip

Recursive functions can be used to compute properties of trees:


val size: Tree<'a> -> int

let rec size tree =


match tree with
| Tree(_,l,r) -> 1 + size l + size r
| Tip -> 1

Here is an example of a constructed tree term and the use of the size function:

> let small = Tree("1”,Tree("2”,Tip,Tip),Tip);;


val small : Tree<string> = Tree ("1",Tree ("2",Tip,Tip),Tip)
> size small;;
val it : int = 2

Symbolic manipulation based on trees is discussed in detail in Chapter SYMBOLIC. Like


records, discriminated union may support members and give implicit implementations of
interfaces. We examine this more closely later in this chapter.
Note: Discriminated unions are a powerful and important construct, and are
crucial when modeling a finite, sealed set of choices, and they are a perfect fit for
many constructs that arise in applications and symbolic analysis libraries.
However, they are, by design, non-extensible, except where that extensibility is
represented by other mechanisms such as having each discriminant carry
additional function values. Subsequent modules may not add new discriminants
to a particular discriminated union. This is deliberate: types such as options and
lists are powerful partly because they put a limit on which possibilities exist.
Extensibility must be defined through alternative techniques, including the use of
records of functions, interfaces and classes.

Using Discriminated Unions as Records


Discriminated union types with only one discriminant make for an effective way to
implement record-like types:
type Point3D = Vector3D of float * float * float
let origin = Vector3D(0.,0.,0.)
let unitX = Vector3D(1.,0.,0.)
let unitY = Vector3D(0.,1.,0.)
let unitZ = Vector3D(0.,0.,1.)
These are particularly effective because they can be decomposed using patterns in the same
way as tuple arguments:
let length (Vector3D(dx,dy,dz)) = sqrt(dx*dx+dy*dy+dz*dz)
This technique is most useful for record-like values where there is some natural order on the
constituent elements of the value (as above), or where the elements have different types.

> let bill = { Name = "Bill"; DateOfBirth = new System.DateTime(1962,09,02) }


val bill : Person = { Name="Bill"; DateOfBirth = 02/09/1962 }

Defining Multiple Types Simultaneously


Multiple types may be involved in a mutually recursive collection of types, including record
types, discriminated unions and abbreviations:
type node =
{ Name : string;
Links : link list }
and link =
| Dangling
| Link of node
Defining Classes and Members
We now look at how to define simple object-oriented classes. In this section we will only
look at how to define concrete classes and members, rather than the use of classes as abstract
values and within inheritance hierarachies. Concrete classes are ones that carry data and
which have a fixed set of members. We discuss abstract values and inheritance hierarchies in
later in this chapter.
In their simplest form, class types are similar to records, though the notation is slightly
different. For example, a vector class can be implemented as follows:
type Vector2D =
class
val DX: float
val DY: float
new(dx,dy) = { DX=dx; DY=dy }
end

> let v1 = new Vector2D(1.0, -1.0);;


val v1 : Vector2D = { DX = 1.0; DY = -1.0 }

Constructors are introduced by the new keyword, and all class values are ultimately
constructed through constructors. These can enforce sophisticated checks and can be
composed with the construction semantics of any inherited class. (In contrast, records are
ultimatley constructed by record expressions that initialize all the fields of the record, and
records do not support inheritance.) For example, the following defines a vector type that
both checks its arguments are positive and pre-computes the length of the vector.
type PositiveVector2D =
class
val DX: float
val DY: float
val Length: float
new(dx,dy) =
if dx < 0.0 || dy < 0.0 then failwith "not positive";
{ DX=dx; DY=dy; Length=sqrt(dx*dx+dy*dy) }
end
Constructors must initialize all fields of the class but can be preceded by a sequence of
checks as above. They may also include a block that gets executed after the initialization of
the fields of the class by adding then followed by an expression at the end of the constructor.
type Vector2D =
class
val DX: float
val DY: float
val mutable computedLength: float option
new(dx,dy,precompute) =
{ DX=dx; DY=dy; Length=None }
then
if precompute then x.Length <- Some(sqrt(dx*dx+dy*dy))
end
The use of then is is particularly important in the context of constructs with two-phase
initialization semantics such as System.Windows.Forms.Control and its related extensions.
Class, record and union types may include all the kinds of members described in Chapter
2, including static methods, instance methods, static properties and instance properties. In
this section we look at how to define a rich set of members on your types.

Method Members
Class types may include all the kinds of members described in Chapter 2, including static
methods, instance methods, static properties and instance properties. Method members are
defined using the keyword member followed by a method name and an argument list. For
example:
type Vector2D =
class
val DX: float
val DY: float
new(dx,dy) = { DX=dx; DY=dy }
member v.Invert() = { DX= -v.DX; DY = -v.DY }
member v.Scale(k) = { DX= k * v.DX; DY = k * v.DY }

static member Create(dx,dy) = new Vector2D(dx,dy)


static member Create' dx dy = { DX= dx; DY = dy }
end
Static members do not act on a target value, and are often used for functions that create
values of the containing type.
Looking at the two Create methods, we see that method members can take arguments in
either “tupled” or “iterated” form. However it is conventional to take all arguments to
methods in tupled form, as this ensures that the members appear in a form that is most natural
when used from other .NET languages.

Property Members
Classes can also include the definitions of property members:
type Vector2D =
class
val DX: float
val DY: float
new(dx,dy) = { DX=dx; DY=dy }
member v.Length = sqrt(v.DX * v.DX + v.DY * v.DY)
end

> let v2 = new Vector2D(3.0, -4.0);;


val v2 : Vector2D = { DX = 3.0; DY = -4.0 }
> v2.Length;;
val it : float = 5.0
In the implementation of the Length property the identifier v stands for the Vector2D value
on which the property is being defined. In many other languages this is called this or self. In
F# you may name this parameter as you see fit. The implementation of properties such as
Length and Angle is executed each time the property is invoked, i.e. properties are syntactic
sugar for function calls. We can see this if we add a side-effect to the implementation:
type MyType =
class
new() = { }
member v.PropertyWithSideEffect = printf “Computing!\n”; 1
end

> let x = new MyType();;


val x : MyType = { }
> x.PropertyWithSideEffect;;
Computing!
val it : int = 1
> x.PropertyWithSideEffect;;
Computing!
val it : int = 1

Setter Properties
The properties shown above are “read-only”, and effectively a shorthand notation for a “get”
function– indeed if you inspect the underlying compiled CIL code for the above you will see
a function called get_Length. Propeties may also have an associated “set” method, and a
longer syntax is used to specify both set and get operations for the property. In the
following we define a mutable representation of a complex number where adjusting the
Angle property rotates the vector while maintaining its overall length:

type MutableVector2D =
class
val mutable DX: float;
val mutable DY: float;
new (dx,dy) = { DX=dx; DY=dy }
member v.Length =
with get () = sqrt(v.DX*v.DX+v.DY*v.DY)
and set len =
let angle = v.Angle
v.DX <- cos(angle)*len;
v.DY <- sin(angle)*len
member c.Angle =
with get () = atan2(v.DY,v.DX)
and set angle =
let len = v.Length
v.DX <- cos(angle)*len;
v.DY <- sin(angle)*len
end
Note that implementations of one member may use other members, e.g. the implementation
of the setter property Angle uses the getter property Length, and vice-versa. The members of
an augmentation thus form a potentially-mutually recursive set of values.

Indexer Properties
Like methods, properties may also take arguments: these are called “indexer” properties. The
most commonly defined indexer property is called Item, and the Item property on an value v
is accessed via the special notation v.[i]. As the notation suggests these are used to
implement the lookup operation on collection types. In the following example we implement
a sparse vector in terms an underlying sorted hash table:
open System.Collections.Generic
type MutableSparseVector =
class
val elems: SortedDictionary<int,float>;
new() = { elems = SortedDictionary<_,_>() }
member v.Add(k,v) = v.elems.Add(k,v)
member v.Item
with get i =
if v.elems.ContainsKey(i) then v.elems.[i]
else 0.0
and set i v =
v.elems.Replace(i,v)
end

> let v = new MutableSparseVector ()


val v : MutableSparseVector
> v.[3];;
val it : float = 0.0
> v.[3] <- 547.0;;
> v.[3];;
val it : float = 547.0

Overloaded Operators
Types may also be augmented with overloaded operators.
open System.Collections.Generic
type Vector2D
class
val DX: float
val DY: float
new(dx,dy) = { DX=dx; DY=dy }
static member (+) ((v1:Vector2D),(v2:Vector2D)) =
new Vector2D(v1.DX + v2.DX, v1.DY + v2.DY)
static member (-) ((v1:Vector),(v2:Vector)) =
new Vector2D(v1.DX - v2.DX, v1.DY - v2.DY)
end
> let v1 = new Vector2D(3.0,4.0);;
val v1 : Vector2D = { DX=3.0; DY=4.0 }
> v1 + v1;;
val it : Vector2D = { DX=4.0; DY=8.0 }

Operator overloading in F# works by defining values that map uses of an operator through to
particular static members on the static types involved in the operation. Defining these
mappings is non-trivial: for example, the F# library includes the following definition for the
(+) operator:
let inline (+) x y = (^a: (static member (+) : ^a * ^b -> ^c) (x,y))
This means that while static members can in principle make use of any operator names, by
default only certain operators mapped through to these static members. You are strongly
encouraged to use certain operators for certain purposes, e.g., it is recommended the
overloaded operator $* be used for multiplying an object such as a vector or matrix by a
scalar value. The F# Informal Language Specification contains a description of the pre-
defined operators and their suggested purposes.

Augmenting Record and Union Types


The member notation is not limited to traditionally OO constructs such as classes and
interfaces: records and discriminated unions may be equipped with additional non-abstract
members. In the example below we define the type Vector as a simple record, augmented
with a property Length.
type Vector2D =
{ DX: float; DY: float }
with
member v.Length = sqrt(v.DX * v.DX + v.DY * v.DY)
end
Here the identifier v stands for the value on which the property is being defined – in many
other languages this is called this or self, while in F# you may name this parameter as you
see fit.

> let x = { DX = 3.0; DY = -4.0 };;


val x : Vector2D = { DX = 3.0; DY = -4.0 }
> x.Length;;
val it : float = 5.0

Discriminated unions and classes may also be given members via augmentations, e.g.:
type Tree<'a> =
| Tree of 'a * Tree<'a> * Tree<'a>
| Tip
with
member t.Size =
match x with
| Tree(_,l,r) -> 1 + l.Size + r.Size
| Tip -> 1
end

Using Augmentations
The with ... end construct is called an “augmentation”. Augmentations may be given
subsequent to the definition of a type, though it is important to note that “additional”
augmentations must be given within the same file or interactive declaration as the type
definition itself. For example a source code file could later contain:
type Vector2D
with
member v.Angle = atan2(v.DX,v.DY)
end

The Angle member will then be in scope and available for use.

Note: augmentations are an effective technique for equipping simple type


definitions with “basic” object-oriented functionality. However, it is often
necessary to include an extra supporting class or module to contain the
remaining non-trivial functionality associated with a type. For example, the
module Microsoft.FSharp.Collections.List contains extra rich functionality
associated with the F# list type. Like all modules under this namespace this may
be opened simply by using open List.

Interfaces, Abstract Members and Object


Expressions
We finish our tour of type definitions with interfaces, which can be thought of as records
of function values, with the added advantage that you can mix interface types together using
interface inheritance. Like other constructs discussed in this chapter, interfaces are often
used or encountered when using or designing .NET frameworks that meet the .NET
Framework design guidelines. In addition some interfaces such as IEnumerable are used
throughout F# programming. New interface types are declared in the following way:
type IPerson =
interface
abstract Name : string
abstract DateOfBirth : System.DateTime
abstract GetChildren() : unit -> IPerson list
end

Note the use of the keyword abstract. Abstract member values are members whose
implementation may vary in implementations the interface type. Here Name and
DateOfBirth are both property member, while GetChildren is a method member. Interfaces
can be implemented using object expressions. Here is a simple example:
> let rhiannon = { new IPerson with
member x.Name = "Rhiannon"
member x.DateOfBirth = new System.DateTime(1997,10,07);
member x.GetChildren() = [] }
val rhiannon : IPerson
> let anna = { new IPerson with
member x.Name = "Anna"
member x.DateOfBirth = new System.DateTime(1968,07,23);
member x.GetChildren() = [ rhiannon ] }
val anna : IPerson

It is .NET convention to prefix the name of all interface types with “I”. However, the use of
interfaces as abstract object values is pervasive in F# OO programming so this convention is
not always followed.
Interfaces may contain all the full range of instance members discussed in the previous
section. For example…

We will seee many examples of more complex interface definitions later in this chapter,
including examples of how classes, records and discriminated union types may implement
interfaces.

Using Interfaces as Abstract Values


The F# libraries and the .NET Framework define a multitude of abstractions that model
common repeating idioms in programming. One example that is used frequently in F#
programming is IEnumerable<'a>, defined in the System.Collections.Generic namespace.
Here the “I” stands for “interface”. Here’s the definition of the IEnumerable abstraction and
the related IEnumerator using F# notation: 1
type IEnumerator<'a> =
interface
abstract Current : 'a
abstract MoveNext : unit -> bool
end

type IEnumerable<'a> =
interface
abstract GetEnumerator : unit -> IEnumerator<'a>
end

An abstract type such as IEnumerable<int> can be implemented by an object expression or


by calling a library construction function such as IEnumerable.unfold.
The above interfaces are defined in a library component that was implemented in another
.NET language (in this case C#), though here we have used the corresponding F# syntax.
Some other very useful predefined F# and .NET abstractions are:

1
In reality IEnumerator<’a> also inherits from the non-generic interface
System.Collection.IEnumerator. For clarity we’ve ignored this here. See the F# library code for full
example implementations of the IEnumerrator type.
* System.IDisposable. An abstraction representing values that own explicitly reclaimable
resources. Not normally passed around as first-class values, since disposability is
really a property of implementations of abstractions. However often used when
building partial implementations of abstractions that automatically execute the disposal
logic at an appropriate point.
* System.IO.Stream. A fundamental I/O abstraction representing a readable or writeable
stream of bytes.

* Microsoft.FSharp.Control.IEvent. An abstraction representing ports into which you can


plug “event listeners”, i.e. callbacks. Some other entity is typically responsible for
“raising” the event and calling all listeners. In F#, .NET events become values of this
type or the related type Microsoft.FSharp.Control.IDelegateEvent, and the module
Microsoft.FSharp.Control.IEvent contains many very useful combinators for
manipulating these values. Like all modules under this namespace this may be opened
simply by using open IEvent.

Interface Inheritance
Interfaces may be arranged in a hierarchy, which gives one way to classify abstractions. For
example, the .NET Framework includes a hierarchical classification of collection types:
IEnumerable<T> is refined by ICollection<T> is refined by IList<T>. Here are the
definitions of these types in F# syntax:

Hierarchical classification through interface inheritance is an important technique but can


be difficult to use well. This is because in practice most collections of related items don’t
have a “canonical” classification, so any particular classification hierarchy and naming
scheme is biased, even if useful. For example, the .NET Framework collection type hierarchy
doesn’t model whether collections are read-only or not: the classification scheme is still
useful, but there are many other classification schemes which you could imagine for the same
set of concepts. While hierarchical modeling is useful, it must also be used with care, as
poorly-designed hierarchies often have to be abandoned late in the software development
life-cycle. For many applications it is adequate to use existing classification hierarchies, in
conjunction with new non-hierarchical record and interface types where new abstract
concepts arise.

Advanced Uses of Object Expressions


Object expressions must give definitions for all unimplemented abstract members and
may not add other additional members. Instead, local state is typically allocated outside the
object expressions, e.g.
open System.Text
let CharCountOuputSink() =
let nchars = ref 0
{ new TextOutputSink() with
member x.WriteChar(c) = (printf “char %d\n” !nchars;
nchars:= ! nchars + 1) }

Classes and Inheritance


Abstract values can be difficult to implement from scratch: for example, a Graphical User
Interface (GUI) component must respond to many different events, often in regular and
predictable ways, and it would be very tedious to have to re-code all this behaviour for each
component. This makes it essential to support the process of creating partial implementations
of abstract values, where the partial implementations can then be completed and/or
customized.
F# supports this in a number of ways, one of which is to use classes. Classes generalize
records with some important additional facilities: abstract member values, inheritance and an
implicit construction syntax. As we saw with interfaces, abstract member values are
members whose implementation may vary in different implementations of a type. Classes
also let you give default implementations to abstract members: extensions and
implementations of the class acquire any default behavior and can modify it. For example,
consider the following class:
type TextOutputSink =
class
abstract WriteChar : char -> unit
abstract WriteString : string -> unit
default x.WriteString(s) = s |> String.iter (fun c -> x.WriteChar(c))
end

This class defines two abstract members WriteChar and WriteString, but gives a default
implementation for WriteString in terms of WriteChar. However, extensions are still free to
override and modify the implementation of WriteString (e.g. some implementations of I/O
will be able to implement block operations much more efficiently than via individual
WriteChar calls). Classes thus let you build types that represent partial or complete
implementations of abstractions.
Before we delve into classes too deeply, we note that there are other ways to achieve
partial implementations of abstractions in F#. In particular, the use of object expressions and
function values lets you model abstractions as records or interfaces and partial
implementations as generator functions:
type ITextOutputSink =
interface
abstract WriteChar : char -> unit
abstract WriteString : string -> unit
end

Before we delve into classes too deeply, we note that there are other ways to achieve partial
implementations of abstractions in F#. In particular, the use of object expressions and
function values lets you model abstractions as records or interfaces and partial
implementations as generator functions:
let simpleTextOutputSink(writeCharFunction) =
{ new TextOutputSink() with
member x.WriteChar(c) = writeChar(c)
member x.WriteString(s) = s |> String.iter (fun c -> x.WriteChar(c)) }

This form of programming is recommended wherever possible, as it gives greater


compositionality and flexibility.

Inheriting classes
Classes may be sub-classes of some existing class type, introduced by the inherit keyword.
Like classes themselves, subclasses take careful design and are best used when modeling
significant new abstract concepts or fragments of default behaviour that form part of the
external interface to a library or framework. For example, the following extension adds two
additional abstract members WriteByte and WriteBytes, and a default implementation for
WriteBytes, an initial implementation for WriteChar, and overrides the implementation of
WriteString to use WriteBytes. The implementations of WriteChar and WriteString use the
.NET functionality to convert the Unicode characters and strings to bytes under the
System.Text.UTF8Encoding, documented in the .NET Framework class libraries.

open System.Text
type ByteOutputStream =
class
inherit TextOutputStream
abstract WriteByte : byte -> unit
abstract WriteBytes : byte[] -> unit
default x.WriteChar(c) = x.WriteBytes(UTF8Encoding.GetBytes([|c|])
default x.WriteString(s) = x.WriteBytes(UTF8Encoding.GetBytes(s)
default x.WriteBytes(b) = b |> Array.iter (fun c -> x.WriteByte(c))
end

The majority of “leaf” extensions of classes can be implemented using object expressions,
e.g.
open System.Text
let StringBufferOuputSink (buf : StringBuffer ) =
{ new TextOutputSink() with
member x.WriteChar(c) = buf.Add(c) }

Here is an example of the use of this function interactively:

> let buf = new System.Text.StringBuffer()


val buf : StringBuffer
> let c = StringBufferOuputSink(buf);;
val c : TextOutputSink
> ["Incy"; " "; "Wincy"; " "; "Spider"] |> List.iter (fun s -> c.WriteString(s));;
> buf.ToString();;
val it : string = "Incy Wincy Spider"

Object expressions must give definitions for all unimplemented abstract members and may
not add other additional members. Instead, local state is typically allocated outside the object
expressions, e.g.
open System.Text
let CharCountOuputSink() =
let nchars = ref 0
{ new TextOutputSink() with
member x.WriteChar(c) = (printf “char %d\n” !nchars;
nchars:= ! nchars + 1) }

This function implements an OutputSink that counts characters, displaying the character
count to the output stream. Object expressions can also be used to model entire families of
leaf classes by accepting function parameters, helping to establish a link between OO
programming and functional programming. For example, the following implements a
TextOutputSink in terms of any function writeChar that provides an implementation of the
abstract member.
let MakeOuputStream(writeChar) =
{ new TextOutputSink() with
member x.WriteChar(c) = writeChar(c) }

This construction function uses function values to build an object of a given shape. Here the
inferred type is:

val MakeOutputStream : (char -> unit) -> TextOutputSink

Classes and Construction


Implicitly Constructed Classes
TBD

Recap: Objects from an F# Perspective


One of the key advances in programming has been the move toward the use of “abstract”
values rather than “concrete” values for large portions of modern software. In this chapter
we’ve seen how to define types, and how to use types to model abstract values. The primary
techniques used to build abstract values in F# are as follows:
* Function types and values. These are used both as abstractions themselves and as a
building block to form other abstractions;
* Record types and values. Records can be used as both “abstract” entites (e.g. records of
function values) and “concrete” entites containing (possibly mutable) data. It is often
important to distinguish between these roles.
* Interface types and object values. Interface types are similar to record types. They are
often implemented by object expressions, and have the added advantage that they may
be arranged in hierarchies. Other types can be declared to implement interface types, as
long as a default implementation of the implementation is provided.
* Class types and object values. Like records, class types can be used to both implement
and define abstractions. In the .NET libraries many important abstractions are modeled
as class types, e.g. the classes Control and Form in the System.Windows.Forms
namespace. A class can be thought of as the combination of an abstraction (i.e. its
members plus its explicit interface implementations) and an implementation. The
implementation may partial (e.g. with unimplemented abstract members) or
configurable (e.g. with mutable properties).
* Module signature types and module implementations. The signatures of modules and
type definitions are a form of abstraction, though they are typically associated with
only one implementation.

In a statically typed language such as F# it’s not too surprising that abstract values are
modeled using types. Indeed, often all the functionality provided by a value is represented by
its static type. However, in some cases further functionality on a value may be discoverable
by using runtime type tests. For example, you may be able to “discover” that a value provides
an implementation of an abstraction by performing a runtime type test.

Note: In OO languages implementing abstractions in multiple ways is commonly


called “polymorphism”, which we will call “polymorphism of implementation”.
Polymorphism of this kind is present throughout F#, and not just with respect to
the OO constructs such as classes and interfaces. Somewhat confusingly, in
functional programming “polymorphism” is used to mean “generic type
parameters”, an orthogonal concept dsicsused in Chapter 2.

F# and Mutation
Sometimes OO programming is presented primarily as a technique for controlling the
complexity of mutable state. However, many of the other traditional concerns of object-
oriented (OO) programming are orthogonal this. For example, higher-level programming
techniques such as interfaces, inheritance and patterns such as publish/subscribe stem from
the OO tradition, and techniques such as functions, type abstraction and functorial operations
such as “map” and “fold” from the value-oriented tradition. None of these techniques have
any fundamental relationship to mutation and identity: for example interfaces and inheritance
can be used very effectively in the context of value-oriented programming. Much of the
success of F# lies in the way that it brings the techniques of OO programming and value-
oriented programming comfortably together.

F# and Implementation Inheritance


In OO programming it has ben traditional to arrange partial implementations of fragments
using implementation inheritance. However, this technique tends to be much less significant
in F# because of the flexibility that functional programming and F# object expressions
provide for defining and sharing implementation fragments. For example, the
Microsoft.FSharp.Collections.IEnumerable module (opened using open IEnumerable)
provides several partial implementations of the IEnumerable<T> abstraction, but does not
use implementation inheritance at all. This is because partial implementations of objects can
be encoded in a number of ways:
* Through construction functions and methods that take function values as arguments
* Through values that implement abstractions and which can be customized by setting
mutable properties
* Through hierarchies of classes
Nevertheless, hierarchies of classes are important in domains such as GUI programming and
the technique is used heavily by .NET libraries written in other .NET languages. For
example, System.Windows.Forms.Control, System.Windows.Forms.Form and
System.Windows.Forms.RichTextBox are part of a hierarchy of visual GUI elements.

Das könnte Ihnen auch gefallen