Chapter 2. PROGRAM STRUCTURE

In this chapter, we introduce some programming paradigms designed to improve program structure. As the topics addressed by this chapter are vast, we shall provide only overviews and then references to literature containing more thorough descriptions and evaluations of the relative merits of the various approaches.

Structured programming is all about managing complexity. Modern computational approaches in all areas of science are intrinsically complicated. Consequently, the efficient structuring of programs is vitally important if this complexity is to be managed in order to produce robust, working programs.

Historically, many different approaches have been used to facilitate the structuring of programs. The simplest approach involves splitting the source code of the program between several different files, known as compilation units. A marginally more sophisticated approach involves the creation of namespaces, allowing variable and function names to be categorised into a hierarchy. More recently, an approach known as object-oriented (OO) programming has become widespread. As we shall see, the F# language supports all of these approaches as well as others. Consequently, F# programmers are encouraged to learn the relative advantages and disadvantages of each approach in order that they may make educated decisions regarding the design of new programs.

Structured programming is not only important in the context of large, complicated programs. In the case of simple programs, understanding the concepts behind structured programming can be instrumental in making efficient use of well-designed libraries.

NESTING

Function and variable definitions can be structured hierarchically within an F# program, allowing some definitions to be globally visible, others to be defined separately in distinct portions of the hierarchy, others to be visible only within a single branch of the hierarchy and others to be refined, specialising them within the hierarchy.

Compared to writing a program as a flat list of function and variable definitions, structuring a program into a hierarchy of definitions allows the number of dependencies within the program to be managed as the size and complexity of the programs grows. This is achieved most simply by nesting definitions. Thus, nesting is the simplest approach to the structuring of programs[8].

For example, the ipow3 function defined in the previous chapter contains a nested definition of a function sqr:

> let ipow3 x =
    let sqr x = x * x
    x * sqr x;;
val ipow3 : int -> int

The nested function sqr is only "visible" within the remainder of the definition of ipow3 and cannot be used outside this function. This capability is particularly useful when part of a function is best factored out into an auxiliary function that has no meaning or use outside its parent function. Nesting an auxiliary function can improve clarity without polluting the external namespace, e.g. with a definition ipow3_aux.

Nesting can be more productively exploited when combined with the factoring of subexpressions, functions and higher-order functions.

FACTORING

The concept of program factoring is best introduced in relation to the conventional factoring of mathematical expressions. When creating a complicated mathematical derivation, the ability to factor subexpressions, typically by introducing a substitution, is a productive way to manage the incidental complexity of the problem.

Factoring out common subexpressions

For example, the following function definition contains several duplicates of the subexpression x — 1:

Factoring out common subexpressions

By factoring out a subexpression a(x), this expression can be simplified:

Factoring out common subexpressions

where a(x) = x — 1.

The factoring of subexpressions, such as x-1, is the simplest form of factoring available to a programmer. The F# function equivalent to the original, unfactored expression is:

> let f x =
    (x - 1.0 - (x - 1.0) * (x - 1.0)) ** (x - 1.0);;
val f : float -> float
> f 5.0;;
val it : float = 20736.0

The F# function equivalent to the factored form is:

> let f x =
    let a = x - 1.0
    (a - a * a) ** a;;
val f : float -> float

> f 5.0;;
val it : float = 20736.0

By simplifying expressions, factoring is a means to manage the complexity of a program. The previous example only factors a subexpression but, in functional languages such as F#, it is possible to factor out higher-order functions.

Factoring out higher-order functions

As we have just seen, the ability to factor out common subexpressions can be useful. However, functions are first-class values in functional programming languages like F# and, consequently, it is also possible to factor out common sub-functions. In fact, this technique turns out to be an incredibly powerful approach to software engineering that is of particular importance in the context of scientific computing.

Consider the following functions that compute the sum and the product of a semi-inclusive range of integers [X0, X1), respectively:

> let rec sum_range xO xl =
    if xO = xl then 0 else xO + sum_range (xO + 1) xl;;
val sum_range : int -> int -> int
The fold_range function can be used to accumulate the result of applying a function f to a contiguous sequence of integers, in this case the sequence [1,9).

Figure 2.1. The fold_range function can be used to accumulate the result of applying a function f to a contiguous sequence of integers, in this case the sequence [1,9).

> let rec product_range xO xl =
    if xO = xl then 1 else
      xO * product_range (xO + 1) xl;;
val product_range : int -> int -> int

For example, the product_range function may be used to compute 5! as the product of the integers [1,6):

> product_range 1 6;;
val it : int = 120

The sum_range and product_range functions are very similar. Specifically, they both apply a function (integer add + and multiply *, respectively) to 1 before recursively applying themselves to the smaller range [xo + l, xi) until the range contains no integers, i.e. X0 = x1.

This commonality can be factored out as a higher-order function fold_range:

> let rec fold_range f xO xl accu =
    if xO = xl then accu else
      f xO (fold_range f (xO + 1) xl accu);;
val fold_range :
(int -> 'a -> 'a) -> int -> int -> 'a -> 'a

The fo1d_range function accepts a function f, a range specified by two integers x0 and xl and an initial value accu. Application of the fold_range function to mimic the sum_range or product_range functions begins with a base case in accu (0 or 1, respectively). If X0 = x1 then accu is returned as the result. Otherwise, the fold_range function applies f to both 1 and the result of a recursive call with the smaller range [l + l,u). This process, known as a right fold because f is applied to the rightmost integer and the accumulator first, is illustrated in figure 2.1.

The sum_range and product_range functions may then be expressed more simply in terms of the fold_range function by supplying a base case (0 or 1, respectively) and operator (addition or multiplication, respectively)[9]:

> let sum_range xO xl = fold_range (+) xO xl 0;;
val sum_range : int -> int -> int

> let product_range xO xl = fold_range { *) xO xl 1;;
val product_range : int -> int -> int

These functions work in exactly the same way as the originals:

> product_range 1 6;;
val it : int = 120

but their commonality has been factored out into the fold_range function. Note how succinct the definitions of the sum_range and product_range functions have become thanks to this factoring.

In addition to simplifying the definitions of the sum_range and product_range functions, the fold_range function may also be used in new function definitions. For example, the following higher-order function, given a length n and a function f, creates the list containing the n elements (f (0), f (l),..., f(n- 1)):

> let list_init n f =
    fold_range (fun h t -> f h :: t) o n [];;
val list_init : int -> (int -> 'a) -> 'a list

This list_init function uses the fold_range function with a λ-function, an accumulator containing the empty list [] and a range [0,n). The λ-function prepends each f h onto the accumulator t to construct the result. For example, the list_init function can be used to create a list of squares:

> list_init 5 (fun i -> i*i);;
val it : int list = [0; 1; 4; 9; 16]

The functionality of this list_init function is already provided in the F# standard library in the form of the List. init function:

> List.init 5 (fun i -> i*i);;
val it : int list = [0; 1; 4; 9; 16]

In fact, this functionality of the List. init function can be obtained more clearly using sequence expressions and comprehensions (described in section 1.4.4):

> [ for i in 0 .. 4 ->
      i * i];;
val it : int list = [0; 1; 4; 9; 16]

As we have seen, the nesting and factoring of functions and variables can be used to simplify programs and, therefore, to help manage intrinsic complexity. In addition to these approaches to program structuring, the F# language also provides two different constructs designed to encapsulate program segments. We shall now examine the methodology and relative benefits of the forms of encapsulation offered by modules and objects.

Modules

In the F# programming language, substantial sections of related code can be productively encapsulated into modules.

We have already encountered several modules. In particular, the predefined module:

Microsoft.FSharp.Core.Operators

encapsulates many function and type definitions initialized before a program is executed, such as operators on the built-in int, float, bool and string types. Also, the Array module encapsulates function and type definitions related to arrays, such as functions to create arrays (e.g. Array, init) and to count the number elements in an array (Array. length).

In general, modules are used to encapsulate related definitions, i.e. function, type, data, module and object definitions. In addition to simply allowing related definitions to be grouped together, the F# module system allows these groups to form hierarchies and allows interfaces between these groups to be defined, the correct use of which is then enforced by the compiler at compile-time. This can be used to add "safety nets" when programming, to improve program reliability. We shall begin by examining some of the modules available in the core F# library before describing how a program can be structured using modules, stating the syntactic constructs required to create modules and, finally, developing a new module.

By convention, if a module focuses on a single type definition (such as the definition of a set in the Set module) then that type is named t, e.g. Set. t.

In the interests of clarity and correctness, modules provide a well-defined interface known as a signature. The signature of a module declares the contents of the module which are accessible to code outside the module. The code implementing a module is defined in the module structure.

The concepts of module signatures and structures are best introduced by example. Consider a module called FloatRange that encapsulates code for handling ranges on the real number line, represented by a pair of float values. This module will include:

  • A type t to represent a range.

  • A function make to construct a range from a given float pair.

  • A function mem to test a number for membership in a range.

  • Set theoretic operations union and intersection.

A FloatRange module adhering to a given signature may be defined as:

> module FloatRange =

   type t = { x0: float; xl: float }

   let make xO xl =
     if xl >= xO then { xO = x0; xl = xl } else
     invalid_arg "FloatRange.make"

   let mem x r =
     r.xO <= x && x < r.xl
   let order a b =
     if a.xO < b.xl then b, a else a, b
   let union a b =
     let a, b = order a b
     if a.xl < b.xO then [a; b] else
     [make (min a.xO b.xO) (max a.xl b.xl)]
   let inter a b =
     let a, b = order a b
     if a.xl < b.xO then [] else
     [make (max a.xO b.xO) (min a.xl b.xl)];;
module FloatRange : begin
  type t = {xO: float;
            Xl: float;}
  val make : float -> float -> t
  val mem : float -> t -> bool
  val order :t-> t-> t*t
  val union : t -> t -> t list val inter : t -> t -> (float * float) list
end

This FloatRange module encapsulated definitions relating to ranges of real-valued numbers and may be used to create and perform operations upon values representing such ranges. For example, a pair of ranges may be created:

> let a, b =
    FloatRange.make 1.0 3.0, FloatRange.make 2.0 5.0;;
val a : FloatRange.t
val b : FloatRange.t

The union and intersection of these ranges may then be calculated using the union and inter functions provided by the FloatRange module. For example, [1,3) ∪ [2,5) = [1,5):

> FloatRange.union a b;;
val it : FloatRange.t list = [{xO = 1.0; xl = 5.0}]

and [1,3)∩[2,5) = [2,3):

> FloatRange.inter a b;;
val it : FloatRange.t list = [{xO = 2.0; xl = 3.0}]

The F# standard library provides many useful modules, such as the modules used to manipulate data structures described in chapter 3. In fact, the same syntax can be used to access objects, such as the .NET standard library.

Modules can be extended by simply creating a new module with the same name. As the two namespaces are indistinguishable, F# will look in all definitions of a module with a given name starting with the most recent definition.

OBJECTS

The .NET platform is fundamentally an object oriented programming environment. Related definitions are encapsulated in classes of objects under .NET. The F# programming language actually compiles all of the constructs we have seen (e.g. records, variants and modules) into .NET objects. This can be leveraged by augmenting these F# definitions with object-related definitions. Object oriented programming is particularly important when interoperating with .NET libraries, which will be discussed in chapter 9.

Augmentations

Properties and members are written using the syntax:

member object. member args = ...

Record and variant types may be augmented with member functions using the type ... with ... syntax. For example, a record representing a 2D vector may be supplemented with a member to compute the vector's length:

> type vec2 = { x: float; y: float } with
    member r.Length = sqrt(r.x * r.x + r.y * r.y);;

Note that the method name r. Length binds the current object to r in the body of the method definition.

The length of a value of type vec2 may then be computed using the object oriented syntax:

> {x = 3.0; y = 4.0}.Length;;
val it : float =5.0

The member Length, which accepts no arguments, is called a property.

Getters and Setters

Properties can provide both get and set functions. This capability can be used to good effect in this case by allowing the length of a vector to be set:

> type vec2 = {mutable x: float; mutable y: float} with
member r.Length
        with get () =
          sqrt(r.x * r.x + r.y * r.y)

        and set len =
          let s = len / r.Length
          r.x <- s * r.x;
          r.y <- s * r.y;;

Note that the fields of the record have been marked mutable so that they can be set.

For example, setting the length of a vector to one scales the vector to unit length:

> let r = {x = 3.0; y = 4.0};;
val r : vec2

> r.Length <- 1.0;;
val it : unit = ()

> r;;
val it : vec2 = {x=0.6; y=0.8}

As this example has shown, significant functionality can be hidden or abstracted away using contracts like getters and setters. This ability to hide complexity is vital when developing complicated programs. The form of abstraction offered by getters and setters is most useful in the context of GUI programming, where it can be used to provide an editable interface to a GUI that hides the internal representation and communication required to reflect changes in a GUI.

Indexing

Classes, records and variants can also provide an Itern property to allow values to be indexed. For example, we may wish to index the vec2 type by an integer 0 or 1:

> type vec2 = {mutable x: float; mutable y: float} with
    member r.Item
      with get(d) =
        match d with
        | 0 -> r.x
        | 1 -> r.y
        | _ -> invalid_arg "vec2.get"
      and set d v =
        match d with
        | 0 ->r.x<-v
        | 1 -> r.y <- v
        | _ -> invalid_arg "vec2.set";;

For example, setting the x -coordinate of r to 3:

> let r = {x=4.0; y=4.0};;
val r : vec2
> r.[0] >- 3.0;;
val it : unit = ()

> r;;
val it : vec2 = {x=3.0; y=4.0}

The vec2 type can now act as an ordinary container, like an array, but the dimensionality is constrained to be 2. Moreover, this constraint is checked by F#'s static type system. If we had a similar vec3 type then vectors of different dimensionalities will be considered incompatible by the F# type system and any errors confusing vectors of the two different types will be caught at compile time.

Leveraging the static type system in this way helps to catch bugs and improve program reliability and reduce development time. There are many ways that F#'s type system can be exploited and some more interesting methods will be described later in this book.

Operator Overloading

In the interests of brevity and clarity, many operators are overloaded in F#. For example, the + and * operators can be used to add numbers of several different types, including int and float. These overloaded operators can be extended to work on user defined types by augmenting the type with static member functions.

Static member functions are member functions that are not associated with any particular object and are denned using the syntax:

static member member args = ...

For example, our vec2 type may be supplemented with a definition of a static member to provide an overload of the built-in + operator for this type:

> type vec2 = { X: float; y: float } with
    static member (+) (a, b) =
      {x = a.x + b.x; y = a.y + b . y};;

The + operator can then be used to add vectors of the type vec2:

> {x = 2.0; y = 3.0} + {x = 3.0; y = 4.0};;
val it : vec2 = {x = 5.0; y = 7.0}

Many of the built-in operators can be overloaded. See the F# manual for specific details.

Classes

In conventional object-oriented programming languages, the vec2 type would have been defined as an ordinary class. There are two different ways to define ordinary classes in F#.

Explicit constructors

The following definition of the vec2 class uses the conventional style with private variables x_ and y_, members x and y that expose their values publically, two overloaded constructors and a Length property that uses the public members rather than the private variables:

> type vec2 =

    val private x_ : float
    val private y_ : float

    new(x) =
      {x_ = x; y_ = 0.0}

    new(x, y) =
      {x_ = x; y_ = y}
    member r.x = r.x_
    member r.y = r.y_
    member r.Length =
      sqrt(r.x * r.x + r.y * r.y);;

Note the verbosity of this approach compared to the augmented record definition (section 2.4.1).

This two overloaded constructors are defined using the new keyword. The first constructs a vec2 assuming the default y = 0. The second constructs a vec2 from both x and y coordinates.

The public members x and y facilitate abstraction and by using these, rather than the internal private variables x_ and y_ directly, the Length property will continue to work if the internal representation is changed. For example, the internal variables x_ and y_ might be altered to represent the vector is polar coordinates, in which case the public members x and y will be updated to continue to present the vector in cartesian coordinates and the Length property will continue to function correctly. This might be useful if performance is limited by operations that are faster in the polar representation.

In F#, the new keyword is not required when constructing an object. For example, the following creates a vec2 using the second constructor and calculates its length using the Length property:

> vec2(3.0, 4.0).Length;;
val it : float = 5.0

The F# programming language also provides a more concise and functional approach to object definitions when there is a single implicit constructor.

Implicit Constructor

Many classes present only one constructor that is used to create all objects of the class. In F#, a shorthand notation exists that allows an implicit constructor to be defined from a more idiomatic F# definition of the class using let bindings. The vec2 example may be written with the arguments of the constructor in the first line of the type definition:

> type vec2(x : float, y : float) =

    member r.x = x
member r.y = y
       member r.Length =
         sqrt(r.x * r.x + r.y * r.y);;

This shorter definition works in exactly the same way as the previous one except that it provides only one constructor:

> vec2(3.0, 4.0).Length;;
val it : float = 5.0

This object-oriented style of programming is useful when dealing with the interface between F# and other .NET languages. Object oriented programming will be used later in this book for exactly this purpose, particularly when dealing with DirectX and Windows Forms GUI programming in chapter 7 and .NET interoperability in chapters 9 and 11.

Run-time type testing

The F# programming language inherits the use of run-time type information from the underlying .NET platform. This allows interfaces to be dynamically typed and allows the value of a type to be determined at run time. Testing of run-time types is virtually non-existent in self-contained idiomatic F# code but this feature can be very important in the context of interfacing to other .NET languages and libraries. In particular, run-time type testing is used in F# programs to catch general .NET exceptions.

In F#, run-time type tests appear as a special pattern matching construct: ? written in the form:

match object with
| : ? class -> ...

In this case, it is often useful to extract the matched value using a named subpattern (this technique was described in section 1.4.2.2):

match object with
| : ? class as x -> f x

The ability to match values of a particular type is important when handling exceptions generated by other .NET programs or libraries. This technique is used in chapter 7 to handle exceptions generated by the DirectX library.

Boxing

Excessive use of objects is a serious impediment to good performance. Consequently, the .NET platform provides several primitive types that are not represented by objects, such as int and float. In some cases, it can be necessary to force a value of a primitive type into an object oriented representation where its class is derived from the univeral obj class. In F#, this functionality is provided by the box function. For example, boxing an int:

> box 12;;
val it : obj = 12

The ability to represent any value as a type derived from obj is essentially dynamic typing. The use of dynamic typing has many disadvantages, most notably performance degredation and unreliability due to the lack of static checking by the compiler. Consequently, use of dynamic typing should be minimized, limited only to cases where existing interfaces require it to be used. This functionality is discussed later in the context of the DirectX bindings (chapter 7) and the .NET metaprogramming facilities (chapter 9).

FUNCTIONAL DESIGN PATTERNS

Like all programming languages, programs written in F# often contain idiomatic approaches to solving certain types of problem or expressing particular functionality. Many books have been written on design patterns in imperative procedural or object oriented languages but there is little information about design patterns in functional programming and nothing specifically on F#.

This section describes some common styles adopted by F# programmers that will be used in the remainder of this book. The clarity and elegance of these approaches makes them worth learning in their own right.

Combinators

The ability to handle functions as values opens up a whole new way of programming. When making extensive use of functional values, the ability to compose these values in different ways can be very useful.

Higher-order functions and currying are often combined to maximize the utility of function composition. Higher-order functions that work by composing applications of other functions (including their argument functions) are sometimes referred to as combinators [13]. Such functions can be very useful when designed appropriately.

The practical benefits of combinators are most easily elucidated by example. In fact, we have already seen some combinators such as the derivative function d (from section 1.6.4).

The F# language provides three useful combinators that are written as operators:

  • f < < g composes the functions f and g (written f o g in mathematics) such that (f < < g) x is equivalent to f (g x).

  • f > > g is a reverse function composition, such that (f > > g) x is equivalent to g (f x).

  • x | > f is function application written in reverse, equivalent to f x.

These combinators will be used extensively in the remainder of this book, particularly the | > combinator.

A surprising amount can be accomplished using combinators. A simple combinator that applies a function to its argument twice may be written:

> let apply_2 f =
    f < < f;;
val apply_2 : ('a -> 'a) -> ('a -> 'a)

For example, doubling the number five twice is equivalent to multiplication by four:

> apply_2 ((*) 2) 5;;
val it : int = 20

A combinator that applies a function four times may be written elegantly in terms of the existing apply_2 combinator:

> let apply_4 f =
    apply_2 apply_2 f;;
val apply_4 : ('a -> 'a) -> ('a -> 'a)

As a generalization of this, a combinator can be used to nest many applications of the same function:

> let rec nest n f x =
    match n with
    | 0 -> x
    | n -> nest (n - 1) f (f x);;
val nest : int -> ('a -> 'a) -> 'a -> 'a

For example, nest 3 f xgivesf(f(f x)):

> nest 3 ((*) 2) 1;;
val it : int = 8

Recursion itself can be factored out into a combinator by rewriting the target function as a higher-order function that accepts the function that it is to call, rather than as a recursive function. The following y combinator can be used to reconstruct a one-argument recursive function:

> let rec y f x =
    f (y f) x;;
val y : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b

Note that the argument f to the y combinator is itself a combinator. For example, the factorial function may be written in a non-recursive form, accepting another factorial function to call recursively as its first argument:

> let factorial factorial = function
    | 0 -> 1
    | n -> n * factorial(n - 1);;
val factorial : (int -> int) -> int -> int

This process of rewriting a recursive function as a combinator to avoid explicit recursion is known as "untying the recursive knot" and makes it possible to stitch functions together, injecting new functionality between function calls. For example, this will be used to implement recursive memoization in section 6.1.4.2.

Applying the y combinator yields the usual factorial function:

> y factorial 5;;
val it : int = 120

Composing functions using combinators makes it easy to inject new code in between function calls. For example, a function to print the argument can be injected before each invocation of the factorial function:

> y (factorial >> fun f n -> printf "%d
" n; f n) 5;;
5
4
3
2
1
0
val it : int = 120

This style of programming is useful is many circumstances but is particularly importance in the context of asynchronous programming, where entire computations are formed by composing fragments of computation together using exactly this technique. This is so useful that the F# language recently introduced a customized syntax for asynchronous programming known as asynchronous workflows.

The task of measuring the time taken to apply a function to its argument can be productively factored into a time combinator. The .NET Stopwatch class can be used to measure elapsed real time with roughly millisecond (0.001s) accuracy. The following time combinator accepts a function f and its argument x and times how long it takes to apply f to x, printing the time taken and returning the result f x:

> let time f x =
    let timer = new System.Diagnostics.Stopwatch()
    timer.Start()
    try f x finally
    printf "Took %dms
" timer.ElapsedMilliseconds;;
val time : ('a -> 'b) -> 'a -> 'b

Note the use of try . . finally to ensure that the time taken is printed even if the function application f x raises an exception.

For example, the time taken to construct a 106-element list:

> time (fun () -> [1 .. 1000000]) ();;
Took 44 6ms

Computations can also be timed from F# interactive sessions by executing the #time statement:

> #time;;
--> Timing now on
> [1 . . 1000000];;
Real: 00:00:00.619, CPU: 00:00:00.640, GC genO: 6,
genl: 2, gen2: 1

> #time;;

--> Timing now off

This has the advantage that it prints both elapsed real time and CPU time (which will be discussed in section 8) as well as the number of different generations of garbage collection that were invoked (quantifying how much the computation stressed the garbage collector). However, our time combinator has the advantage that it can be placed inside computations uninvasively in order to time certain parts of a computation.

Our time combinator will be used extensively throughout this book to compare the performance of different functions. In particular, chapter 8 elaborates on the time combinator in the context of performance measurement, profiling and optimization.

Maps and folds

section 2.2 introduced the concept of a fold. This is a higher-order function that applies a given function argument to a set of elements, accumulating a result. In the previous example, the fold acted upon an implicit range of integers but folds are most often associated with container types (such as lists and arrays), a subject covered in detail in chapter 3.

Folds over data structures are often categorized into left and right folds, referring to starting with the left-most (first) or starting with the right-most (last) element, respectively.

The addition operator can be folded over a container in order to sum the int elements of a container xs. With a left fold this is written in the form:

fold_left ( + ) 0 xs

With a right fold this is written in the form:

fold_right ( + ) xs 0

Note the position of the initial accumulator 0, on the left for a left-fold and on the right for a right-fold.

For example, if xs is the sequence (1,2,3) then the left-fold computes ((0 + 1) + 2) + 3 whereas a right-fold computes 1 + (2 + (3 + 0)).

Maps are a similar concept. These higher-order functions apply a given function argument / to a set of elements xi to create a new set of elements yi = f(xi). Container types (e.g. lists and arrays) typically provide a map function.

Maps and folds are generic algorithms used in a wide variety of applications. The data structures in the F# standard library provide maps and folds where appropriate, as we shall see in chapter 3.

Developing an application written entirely in F# using Microsoft Visual Studio 2005.

Figure 2.2. Developing an application written entirely in F# using Microsoft Visual Studio 2005.

F# DEVELOPMENT

The simplest way to develop software in F# is to use Microsoft's own Visual Studio and then install Microsoft's F# distribution. Visual Studio is an integrated development environment that makes it easy to develop .NET applications in various languages including F#. Moreover, Visual Studio allows you to build applications that are written in several different programming languages. Figure 2.2 shows one of the applications from The F#.NET Journal being developed using Visual Studio 2005.

In Visual Studio terminology, an application or program is a solution and a solution may be composed of several projects. Projects may be written in different languages and refer to each other. Both individual projects and complete solutions can be built to create executables that can then be run.

Visual Studio provides graphical throwback of the type information inferred by the F# compiler: hovering the mouse over the definition of a variable r in the source code brings up a tooltip giving the inferred type of r.

Figure 2.3. Visual Studio provides graphical throwback of the type information inferred by the F# compiler: hovering the mouse over the definition of a variable r in the source code brings up a tooltip giving the inferred type of r.

Creating an F# project

To create a new project in Visual Studio click File

Creating an F# project

To add an F# source code file to the new project, right-click on the project's name in the Solution Explorer pane and select Add

Creating an F# project

An important advantage of Visual Studio is the graphical throwback of inferred type information. For example, figure 2.3 shows the inferred type vec2 of the variable r in some F# source code. As type information is not explicit in F#, this functionality is extremely useful when developing F# programs.

Once written, F# source code can be executed in two different ways: a solution or project may be built to create an executable or the code may be entered into a running F# interactive mode.

Building executabies

A Visual Studio solution or project may be built to create an executable. To build an executable press CTRL+SHIFT+B. More often, solutions are built and executed immediately. This can be done in one step by pressing F5 to build and run a solution in debug mode or CTRL+F5 to build and run a solution in release mode. As the names imply, debug mode can provide more information when a program goes wrong as opposed to release mode which executes more quickly.

A project's properties page allows the compiler to be controlled.

Figure 2.4. A project's properties page allows the compiler to be controlled.

Various options can be adjusted in the Project Properties window (see figure 2.4), to control the way an executable is built:

  • If an executable is to be distributed to third parties (e.g. when writing a commercial application), the project should be compiled with the option - - standalone in order to compile in the F# libraries that are not included in .NET itself.

  • Graphical programs do not want the console window that appears by default. This can be removed either by changing the proj ect type from EXE to WINEXE or by compiling with the option - -target -winexe.

  • Libraries used by a project may be described on the compile line using the syntax - r name . dl1 or in the source code using the #r pragma.

These options are set in the Project Properties window.

Although a detailed tutorial on Visual Studio is beyond the scope of this book it is worth mentioning the subject of debugging.

Debugging

The static typing provided by the F# language goes a long way in eliminating errors in programs before they are run. However, there are still cases when a program will fail during its execution. In such cases, a detailed report of the problem can be essential in debugging the application.

Visual Studio provides a great deal of debugging support to help the programmer. In the case of F#, an application that fails (typically due to the raising of an exception that is never caught and handled) gives immediate throwback on where the exception came from and what it contained. Breakpoints can be set the in source code, to stop execution when a particular point is reached. The values of variables may then be inspected.

One of the most useful capabilities of the Visual Studio debugger is the ability to pause a program when an exception is thrown even if it is subsequently caught. This functionality is enabled through the Debug

Debugging

In addition to building executable applications, F# provides a powerful interactive mode that allows code to be executed without being built beforehand.

Interactive mode

In addition to conventional project building, F# provides an interactive way to execute code, called the F# interactive mode. This mode is useful for trying examples when first learning the F# programming language and for testing snippets of code from programs during development.

Scientists and engineers typically use two separate kinds of programming language:

  1. Fast compiled languages like C and Fortran.

  2. Expressive high-level interactive languages with integrated visualization like Matlab and Mathematica.

F# is the first language to combine the best of both worlds with the F# interactive mode.

Before the F# interactive mode can be used within Visual Studio, it must be selected in the Add-in Manager (available from the menu Tools

Interactive mode

The F# interactive mode can be started by pressing ALT+ENTER in the editor window. Lines of code from the editor can be evaluated in an interactive mode by pressing ALT+'. Selected blocks of code can be evaluated in an interactive mode by pressing ALT+ENTER. When typing directly into an F# interactive session, evaluation is forced by ending the last line with;;.

Double semicolons

The examples given so far in this book have been written in the style of the F# interactive mode and, in particular, have ended with;; to force evaluation. These double semicolons are not necessary in compiled code (i.e. in .fs and .fsx files).

The Add-in Manager is used to provide the F# interactive mode.

Figure 2.5. The Add-in Manager is used to provide the F# interactive mode.

For example, the two function definitions can be interpreted simultaneously in an interactive session, with only a single;; at the end:

> let fl x =
    if x < 3 then print_endline "Less than three"
  let f2 x =
    if x < 3 then "Less" else "Greater";;
val fl : int -> unit
val f2 : int -> string

When compiled, programs can be written entirely without;;.

Loading F# modules

Module definitions from compilation units (the .fs and .fsx files) can be loaded into a running F# interactive session using the #use and #load directives.

The #use directive interprets the contents of a compilation unit directly, defining everything from a module directly into the running interactive session. For example, assuming a file "foo.fs" contains a definition for x, invoking the #use directive:

> #USe "foo.fS";;

causes the definition of x to be available from the interactive session. The #load directive interprets the contents of a compilation unit as a module. For example:

> #load "foo.fs";;

makes the definition of x available as Foo. x.

Pretty printing

The way an F# interactive session prints values of different types can be altered by registering new functions to convert values into strings with the AddPrinter member function of the fsi object.

The following variant type might be used to represent lengths in different units, starting with one type constructor to represent lengths in meters:

> type length = Meters of float;;

Values of this type may be pretty printed with:

> fsi.AddPrinter(fun (Meters x) -> sprintf "%fm" x);;

For example:

> Meters 5.4;;
val it : length = 5.400000m

The ability to control the printing of values by an F# interactive session will be used in the remainder of this book. In particular, chapter 7 will use this approach is to spawn windows visualizing values that represent graphics.

C# interoperability

One of the great benefits of the common language run-time that underpins .NET is the ability to interoperate with dynamically linked libraries (DLLs) written in other .NET languages. In particular, this opens up the wealth of tools available for mainstream languages like C# including the GUI Windows Forms designers integrated into Visual Studio and the automatic compilation of web services into C# functions. A specific example of this will be discussed in chapter 10 in the context of web services.

An F# program that requires a C# library is easily created using Visual Studio:

  1. Create a C# project inside a new Solution directory (see figure 2.6).

  2. Create an F# project in the same solution (see figure 2.7).

  3. Set the startup project of the solution to the F# program rather than the DLL (see figure 2.8) by right clicking on the solution in the "Solution Explorer" window and selecting Properties from the menu.

  4. Build the C# DLL (right click on it in Solution Explorer and select "Rebuild") and reference it from the F# program.

The name.dll DLL can be referenced from the source code of an F# program with:

#r "name.dll"

In this case, the directory of the DLL must be added to the search path before the DLL is referenced.

Creating a new C# class library project called ClassLibraryl inside a new solution called Interop.

Figure 2.6. Creating a new C# class library project called ClassLibraryl inside a new solution called Interop.

Creating a new F# project called Projectl also inside the Interop solution.

Figure 2.7. Creating a new F# project called Projectl also inside the Interop solution.

Setting the startup project ofthe Interop solution to the F# project Project1 rather than the C# project ClassLibraryl as a DLL cannot be used to start an application .

Figure 2.8. Setting the startup project ofthe Interop solution to the F# project Project1 rather than the C# project ClassLibraryl as a DLL cannot be used to start an application .

For example, the following adds a member function foo () to the C# DLL:

using System;
using System.Collections.Generic;
using System.Text;

namespace ClassLibraryl
{
    public class Classl
    {
        public static void foo()
        {
            Console.WriteLine("foo");
        }
    }
}

The following F# source code adds the default location of the debug build of the C# DLL to the search path, references the DLL and then calls the foo () function:

#light
System.Environment.CurrentDirectory <-
  __SOURCE_DIRECTORY__
#1 @"..ClassLibrarylinDebug"
#r "ClassLibraryl.dll"
ClassLibraryl.Classl.foo()

The syntax @" ..." is used to quote a string without having to escape it manually. This can be compiled or run interactively. When evaluated in an interactive solution, this F# code produces the following output:

> #light;;

> System.Environment.CurrentDirectory <-
    __SOURCE_DIRECTORY__;;
val it : unit = ()

> #I @"..ClassLibrarylinDebug";;
--> Added 'C:Visual Studio 2005ProjectsInterop
Projectl..ClassLibrarylinDebug' to library include
path

> #r "ClassLibraryl.dll";;
--> Referenced 'C:Visual Studio 2005ProjectsInterop
Proj ectl..ClassLibrarylinDebugClassLibraryl.dll'

> ClassLibraryl.Classl.foo();;
Binding session to 'C:Visual Studio 2005Projects
InteropProjectl..ClassLibrarylinDebug
ClassLibraryl.dll'...
foo

val it : unit = ()

Note that the invocation of the C# function from F# prints "foo" as expected. The same procedure can be used to add C# projects containing autogenerated code from the Windows Forms designer or by adding Web References to the C# project.



[8] Remarkably, many other languages, including C and C++, do not allow nesting of function and variable definitions.

[9] The non-infix form of the + operator is written ( + ), i.e. ( + ) a b is equivalent to a + b. Note the spaces to avoid ( * ) from being interpreted as the start of a comment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset