F# Overview (III.) - Imperative and Object-Oriented Programming
In the third part of the F# Overview article series, we will look at language features that are mostly well known, because they are present in most of the currently used programming languages. Indeed, I'm talking about imperative programming, which is a common way for storing and manipulating application data and about object oriented programming which is used for structuring complex programs.
In general, F# tries to make using them together with the functional constructs described in the previous part [^] as natural as possible, which yields several very powerful constructs.
Imperative Programming and Mutable Values
Similarly as ML and OCaml, F# adopts an eager evaluation mechanism, which means that a code written using
sequencing operator is executed in the same order in which it is written and expressions given as an arguments
to a function are evaluated before calling the function (this mechanism is used in most imperative languages
including C#, Java or Python). This makes it semantically reasonable to support
imperative programming features in a functional language. As already mentioned, the F# value bindings are by default
immutable, so to make a variable mutable the mutable
keyword has to be used. Additionally F# supports a
few imperative language constructs (like for
and while
), which are expressions of type unit
:
> // Imperative factorial calculation
let n = 10
let mutable res = 1
for n = 2 to n do
res <- res * n
// Return the result
res;;
val it : int = 3628800
The use of the eager evaluation and the ability to use mutable values makes it very easy to interoperate with other
.NET languages (that rely on the use of mutable state), which is an important aspect of the F# language. In addition
it is also possible to use the mutable
keyword for creating a record type with a mutable field.
Arrays
As mentioned earlier, another type of value that can be mutated is .NET array. Arrays can be either created
using [| .. |]
expressions (in the following example we use it together with a range expression,
which initializes an array with a range) or using functions from the Array
module, for example
Array.create
. Similarly to the mutable variables introduced in the previous section, the value of
an array element can be modified using the <-
operator:
> let arr = [| 1 .. 10 |]
val arr : array<int>
> for i = 0 to 9 do
arr.[i] <- 11 - arr.[i]
(...)
> arr;;
val it : array<int> = [| 10; 9; 8; 7; 6; 5; 4; 3; 2; 1 |]
.NET interoperability
The .NET BCL is built in an object oriented way, so the ability to work with existing classes is essential for the
interoperability. Many (in fact almost all) of the classes are also mutable, so the eager evaluation and the support
for side-effects are two key features when working with any .NET library. The following example demonstrates working
with the mutable generic ResizeArray<T>
type from the BCL (ResizeArray
is an alias for a type
System.Collections.Generic.List
to avoid a confusion with the F# list
type):
> let list = new ResizeArray<_>()
list.Add("hello")
list.Add("world")
Seq.to_list list;;
val it : string list = ["hello"; "world"]
As you can see, we used the underscore symbol when creating an instance of the generic type, because the
type inference algorithm in F# can deduce the type argument from the code (in this example it infers that the
type argument is string
, because the Add
method is called with a string
as an argument). After creating an instance we used Add
method to modify the list and add two new items.
Finally, we used a Seq.to_list
function to convert the collection to the F# list.
As a fully compatible .NET language, F# also provides a way for declaring its own classes (called object types in F#), which are compiled into CLR classes or interfaces and therefore the types can be accessed from any other .NET language as well as used to extend classes written in other .NET languages. This is an important feature that allows accessing complex .NET libraries like Windows Forms or ASP.NET from F#.
Object Oriented Programming
Object Types
Object oriented constructs in F# are compatible with the OO support in .NET CLR, which implies that F# supports
single implementation inheritance (a class can have one base class), multiple interface inheritance (a class
can implement several interfaces and an interface can inherit from multiple interfaces), subtyping (an inherited class
can be casted to the base class type) and dynamic type tests (it is possible to test whether a value is a
value of an inherited class casted to a base type). Finally, all object types share a common base class which is called
obj
in F# and is an alias to the CLR type System.Object
.
F# object types can have fields, constructors, methods and properties (a property is just a syntactic sugar for getter and setter methods). The following example introduces the F# syntax for object types:
type MyCell(n:int) =
let mutable data = n + 1
do printf "Creating MyCell(%d)" n
member x.Data
with get() = data
and set(v) = data <- v
member x.Print() =
printf "Data: %d" data
override x.ToString() =
sprintf "(Data: %d)" data
static member FromInt(n) =
MyCell(n)
The object type MyCell
has a mutable field called data
, a property called Data
,
an instance method Print
, a static method FromInt
and the type also contains one
overridden method called ToString
, which is inherited from the obj
type and returns
a string representation of the object. Finally, the type has an implicit constructor. Implicit constructors are
syntactical feature which allows us to place the constructor code directly inside the type declaration and to write the
constructor arguments as part of the type
construct. In our example, the constructor initializes the mutable
field and prints a string as a side effect. F# also supports explicit constructors that have similar syntax
as other members, but these are needed rarely.
In the previous example we implemented a concrete object type (a class), which means that it is possible to create an instance of the type and call its methods in the code. In the next example we will look at declaration of an interface (called abstract object type in F#). As you can see, it is similar to the declaration of a class:
type AnyCell =
abstract Value : int with get, set
abstract Print : unit -> unit
The interesting concept in the F# object oriented support is that it is not needed to explicitly specify whether the
object type is abstract (interface), concrete (class) or partially implemented (class with abstract methods),
because the F# complier infers this automatically depending on the members of the type.
Abstract object types (interfaces) can be implemented by a concrete object type (class) or by an object expression,
which will be discussed shortly. When implementing an interface in an object type we use the
interface .. with
construct and define the members required by the interface. Note that the
indentation is significant in the lightweight F# syntax, meaning that the members implementing
the interface type have to be indented further:
type ImplementCell(n:int) =
let mutable data = n + 1
interface AnyCell with
member x.Print() = printf "Data: %d" data
member x.Value
with get() = data
and set(v) = data <- v
The type casts supported by F# are upcast, used for casting an object to a base type or to an implemented interface type
(written as o :> TargetType
), downcast, used for casting back from a base type
(written as o :?> TargetType
), which throws an exception when the value isn’t a value of the specified type
and finally, a dynamic type test (written as o :? TargetType
), which tests whether
a value can be casted to a specified type.
Object expressions
As already mentioned, abstract types can be also implemented by an object expression. This allows us to implement
an abstract type without creating a concrete type and it is particularly useful when you need to return an implementation
of a certain interface from a function or build an implementation on the fly using functions already defined somewhere
else in your program. The following example implements the AnyCell
type:
> let newCell n =
let data = ref n
{ new AnyCell with
member x.Print() = printf "Data: %d" (!data)
member x.Value
with get() = !data
and set(v) = data:=v };;
val newCell : int -> AnyCell
In this code we created a function that takes an initial value as an argument and returns a cell holding
this value. In this example we use one more type of mutable values available in F#, called reference cell,
which are similar to a mutable values, but more flexible (the F# compiler doesn’t allow using an ordinary mutable value
in this example). A mutable cell is created by a ref
function taking an initial value. The
value is accessed using a prefix !
operator and can be modified using :=
operator.
When implementing the abstract type, we use a new ... with
construct with members implementing
the functionality required by the abstract type (an object expression can’t add any members).
In this example we need a reference cell to hold the value, so the cell is declared in a function and captured
in a closure, which means that it will exist until the returned object will be garbage collected.
Adding Members to F# Types
Probably the most important advantage of using object types is that they hide an implementation of the type, which makes it possible to modify the implementation without breaking the existing code that uses them. On the other side, basic F# types like discriminated unions or records expose the implementation details, which can be problematic in some cases, especially when developing a larger application. Also, the dot-notation used with object types makes it very easy to discover operations supported by the type. To bridge this problem, F# allows adding members to both discriminated unions and record types:
> type Variant =
| Num of int
| Str of string with
member x.Print() =
match x with
| Num(n) -> printf "Num %d" n
| Str(s) -> printf "Str %s" s;;
(...)
> let v = Num 42
v.Print();;
Num 42
In this example we declared a type called Variant
which can contain either
a number or a string value and added a member Print
that can be invoked using
dot-notation. Aside from adding members (both methods an properties) it is also possible to
implement an abstract object type by a record or discriminated union using the interface ... with
syntax mentioned earlier.
Rather than writing all code using member
syntax, it is often more elegant to implement the
functionality associated with an F# type in a function and then use type augmentations to make
this functionality available as a member via dot-notation. This is a pattern used very often in the F# library
implementation and I personally believe that it makes the code more readable.
The following example re-implements the Variant
type using this pattern:
type Variant =
| Num of int
| Str of string
let print x =
match x with
| Num(n) -> printf "Num %d" n
| Str(s) -> printf "Str %s" s
type Variant with
member x.Print() = print x
The construct type ... with
is a type augmentation, which adds the member Print
to a type declared earlier in the code. The type augmentation has to be included in a same compilation unit as the declared type -
usually in a same file. It is also possible to attach extension members to a type declared in a
different compilation unit - the main difference is that these members are just a syntactical sugar and are not
a part of the original type, meaning that they can't access any implementation details of the type. The only reason
for using extension members is that they make your function for working with the type available using the dot-notation,
which can simplify the code a lot and it will be easier to find the function (for example it will be available in the Visual Studio
IntelliSense). When declaring an extension member you use the same syntax as for type augmentations with the
difference that the name of the type has to be fully qualified as demonstrated in the following example:
> type System.Collections.Generic.List<'a> with
member x.ToList() = Seq.to_list x;
(...)
> let r = new ResizeArray<_>()
r.Add(1)
r.Add(2)
r.ToList();;
val it : list<int> = [ 1; 2 ]
In this example we use extension members to add a ToList
method to an existing .NET generic
type. Note that when declaring the extension members we have to use the original type name and not the F#
alias. You should also bear in mind that extension members are resolved by the F# compiler and so calling them
from C# will not be easily possible. In general, extension members are not declared very often, but some parts of
the F# library (for example the features for asynchronous and parallel programming) use them.
Article Series Links
- F# Overview (I.) - Introduction
- F# Overview (II.) - Functional Programming
- F# Overview (III.) - Object Oriented and Imperative Programming
- F# Overview (IV.) - Language Oriented Programming
Published: Saturday, 3 November 2007, 12:00 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: functional, f#