Chapter 7. Encapsulating and Packaging Your Code

Packaging code and making it available for people and programs to use is a key part of making the best use of F#. In this book, you've already seen many of the constructs to help do this: functions, objects, type definitions, modules, namespaces, and assemblies. However, in some cases you've encountered these only peripherally when using the .NET Base Class Library (BCL). This chapter covers these constructs from the perspective of code organization and packaging.

Packaging code has four distinct but related meanings:

  • Organizing code into sensible entities using namespaces, types, and modules.

  • Encapsulating internal data structures and implementation details by making them private.

  • Assembling code and data as a component, which on .NET is called an assembly. An assembly is one or more DLL or EXE packaged together with supporting data as a single logical unit.

  • Deploying one or more assemblies, such as a web application or framework, often with an installer, for use on client machines, on web servers, or for download over the Web.

The first two of these topics are associated with the F# language, and the last is more associated with the pragmatics of deploying, installing, configuring, and maintaining software. The third lies in between, because a .NET assembly can act as both a unit of encapsulation and a unit of deployment.

Knowing these techniques isn't the same as knowing how to use them effectively, or indeed when to use them. At the end of this chapter, we cover some of the different kinds of .NET software you may write with F# and how you can organize and package your code for these different cases.

Hiding Things Away

In all kinds of software, it's common to hide implementation details of data structures and algorithms behind encapsulation boundaries. Encapsulation is a fundamental technique when writing software and is possibly the most important idea associated with object-oriented (OO) programming.

For this book's purposes, encapsulation means hiding implementation details behind well-defined boundaries. This lets you enforce consistency properties and makes the structure of a program easier to manage. It also lets an implementation evolve over time. A good rule of thumb is that anything you don't want used directly by client code should be hidden.

Later, this chapter explains how encapsulation applies when you're building assemblies, frameworks, and applications. In the extreme, you may even be ensuring that your code is secure when used in partial trust mode—in other words, that it can't be inadvertently or deliberately used to achieve malicious results when used as a library by code that doesn't have full permissions. However, the most important kind of encapsulation is the day-to-day business of hiding the internal implementation details of functions, objects, types, and modules. The primary techniques used to do this are as follows:

  • Local definitions

  • Accessibility annotations

  • Explicit signatures

We cover the first two of these techniques next, and we cover explicit signatures in the "Using Signature Types and Files" section later in this chapter.

Hiding Things with Local Definitions

The easiest way to hide definitions is to make them local to expressions or constructed class definitions using inner let bindings. These aren't directly accessible from outside their scope. This technique is frequently used to hide state and other computed values inside the implementations of functions and objects. Let's begin with a simple example. Here is the definition of a function that incorporates a single item of encapsulated state:

let generateTicket =
    let count = ref 0
    (fun () -> incr count; !count)

If you examine this definition, you see that the generateTicket function isn't defined immediately as a function but instead first declares a local element of state called count and then returns a function value that refers to this state. Each time the function value is called, count is incremented and dereferenced, but the reference cell is never published outside the function implementation and is thus encapsulated.

Encapsulation through local definitions is a particularly powerful technique in F# when used in conjunction with object expressions. For example, Listing 7-1 shows the definition of an object interface type called IPeekPoke and a function that implements objects of this type using an object expression.

Example 7.1. Implementing Objects with Encapsulated State

type IPeekPoke =
    abstract member Peek: unit -> int
    abstract member Poke: int -> unit

let makeCounter initialState =
    let state = ref initialState
    { new IPeekPoke with
        member x.Poke(n) = state := !state + n
        member x.Peek() = !state  }

The type of the function Counter is as follows:

val makeCounter : int -> IPeekPoke

As with the earlier generateTicket function, the internal state for each object generated by the makeCounter function is hidden and accessible only via the published Peek and Poke methods.

The previous examples show how to combine let bindings with anonymous functions and object expressions. You saw in Chapter 6 how let bindings can also be used in constructed class types. For example, Listing 7-2 shows a constructed class type with private mutable state count and that publishes two methods: Next and Reset.

Example 7.2. A Type for Objects with Encapsulated State

type TicketGenerator() =
    // Note: let bindings in a type definition are implicitly private to the object
    // being constructed. Members are implicitly public.
    let mutable count = 0

    member x.Next() =
        count <- count + 1;
        count

    member x.Reset () =
        count <- 0

The variable count is implicitly private to the object being constructed and is hence hidden from outside consumers. By default, other F# definitions are public, which means they're accessible throughout their scope.

Frequently, more than one item of state is hidden behind an encapsulation boundary. For example, the following code shows a function makeAverager that uses an object expression and two local elements of state, count and total, to implement an instance of the object interface type IStatistic:

type IStatistic<'T,'U> =
    abstract Record : 'T -> unit
    abstract Value : 'U

let makeAverager(toFloat: 'T -> float) =
    let count = ref 0
    let total = ref 0.0
    { new IStatistic<'a,float> with
          member stat.Record(x) = incr count; total := !total + toFloat x
          member stat.Value = (!total / float !count) }

The inferred types here are as follows:

type IStatistic<'T,'U> =
    abstract Record : 'T -> unit
    abstract Value: 'U
val makeAverager : ('T -> float) -> IStatistic<'T,float>

The internal state is held in values count and total and is, once again, encapsulated.

Note

Most of the examples of encapsulation in this chapter show ways to hide mutable state behind encapsulation boundaries. However, encapsulation can be just as important for immutable constructs, especially in larger software components. For example, the implementation of the immutable System.DateTime type in the .NET BCL hides the way the date and time are stored internally but reveals the information via properties. This allows the .NET designers to adjust the implementation of this type between releases of the .NET Framework if necessary.

Hiding Things with Accessibility Annotations

Local definitions are good for hiding most implementation details. However, sometimes you may need definitions that are local to a module, an assembly, or a group of assemblies. You can change the default accessibility of an item by using an accessibility annotation to restrict the code locations that can use a construct. These indicate what is private or partially private to a module, file, or assembly. The primary accessibility annotations are private, internal, and public:

  • private makes a construct private to the enclosing type definition/module.

  • internal makes a construct private to the enclosing assembly (DLL or EXE).

  • public makes a construct available globally, which is the default for most constructs.

Accessibility annotations are placed immediately prior to the name of the construct. Listing 7-3 shows how to protect an internal table of data in a module using accessibility annotations.

Example 7.3. Protecting a Table Using Accessibility Annotations

open System

module public VisitorCredentials =

   /// The internal table of permitted visitors and the
   /// days they are allowed to visit.
   let private  visitorTable =
       dict [ ("Anna",    set [DayOfWeek.Tuesday; DayOfWeek.Wednesday]);
              ("Carolyn", set [DayOfWeek.Friday]) ]
   /// This is the function to check if a person is a permitted visitor.
   /// Note: this is public and can be used by external code
   let public checkVisitor(person) =
       visitorTable.ContainsKey(person) &&
       visitorTable.[person].Contains(DateTime.Today.DayOfWeek)

   /// This is the function to return all known permitted visitors.
   /// Note: this is internal and can only be used by code in this assembly.
   let internal allKnownVisitors() =
       visitorTable.Keys

The private table is visitorTable. Attempting to access this value from another module gives a type-checking error. The function checkVisitor is marked public and is thus available globally. The function allKnownVisitors is available only within the same assembly (or the same F# Interactive session) as the definition of the VisitorCredentials module. Note that you could drop the public annotations from checkVisitor and VisitorCredentials because these declarations are public by default.

Accessibility annotations are often used to hide state or handles to resources such as files. In Listing 7-4, you protect a single reference cell containing a value that alternates between Tick and Tock. This example uses an internal event, a technique covered in more detail in Chapter 8.

Example 7.4. Protecting Internal State Using Accessibility Annotations

module public GlobalClock =

    type TickTock = Tick | Tock

    let private clock = ref Tick

    let private tick = new Event<TickTock>()

    let internal oneTick() =
        (clock := match !clock with Tick -> Tock | Tock -> Tick);
        tick.Trigger (!clock)

    let tickEvent = tick.Publish

module internal TickTockDriver =
    open System.Threading
    let timer = new Timer(callback=(fun _ -> GlobalClock.oneTick()),
                          state=null,dueTime=0,period=100)

In Listing 7-4, the private state is clock. The assembly-internal module TickTockDriver uses the System.Threading.Timer class to drive the alternation of the state via the internal function oneTick. The GlobalClock module publishes one IEvent value, tickEvent, which any client can use to add handlers to listen for the event. Note that you can give different accessibility annotations for different identifiers bound in the same pattern. The sample uses the Event type, covered in more detail in Chapter 8.

Another assembly can now contain the following code that adds a handler to TickEvent:

module TickTockListener =
   do GlobalClock.tickEvent.Add(function
       | GlobalClock.Tick -> printf "tick!"
       | GlobalClock.Tock -> System.Windows.Forms.MessageBox.Show "tock!" |> ignore)

You can add accessibility annotations quite a number of places in F# code:

  • On let, and extern definitions in modules, and in individual identifiers in patterns

  • On new(...) object constructor definitions

  • On member definitions associated with types

  • On module definitions

  • On type definitions associated with types

  • On primary constructors, such as type C private (...) = ...

Note

You can add accessibility annotations to type abbreviations. However, the abbreviation is still just an abbreviation—only the name is hidden, not the actual equivalence. That is, if you define a type abbreviation such as type private label = int, then all users of the type label know that it's really just an abbreviation for int and not a distinct type definition of its own. This is because .NET provides no way to hide type abbreviations; the F# compiler expands type abbreviations in the underlying generated .NET IL code.

Listing 7-5 shows a type where some methods and properties are labeled public but the methods that mutate the underlying collection (Add and the set method associated with the Item property) are labeled internal.

Example 7.5. Making Property Setters Internal to a Type Definition

open System.Collections.Generic

type public SparseVector () =

    let elems = new SortedDictionary<int,float>()

    member internal vec.Add (k,v) = elems.Add(k,v)

    member public vec.Count = elems.Keys.Count
    member vec.Item
        with public get i =
            if elems.ContainsKey(i) then elems.[i]
            else 0.0
        and internal set i v =
            elems.[i] <- v

Note

In class types, let bindings in types are private to the object being constructed, and all member bindings are public—they have the same accessibility as the type definition. This is a useful default because it corresponds to the common situation where internal implementation details are fully private and published constructs are available widely, and because omitting accessibility annotations makes code more readable in the common case. When you start to add more specific accessibility annotations, such as by making individual members internal or private, then it's useful to explicitly mark all members with accessibility annotations. Doing so makes your code more readable because readers don't have to remember that unmarked members are public. You can leave the remaining let bindings unmarked and implicitly private. In short, we recommend that if you mark any members of a type with accessibility annotations, you should mark them all.

Using Namespaces and Modules

An important organizational technique is to give sensible qualified names to your types and values. A qualified name is, for example, Microsoft.FSharp.Collections.List (for the F# list type) or System.IO.StreamReader (for one of the types in the .NET Framework BCL). Qualified names are particularly important when you're writing frameworks to be used by other people and are also a useful way of organizing your own code.

You give types and functions qualified names by placing them in namespaces, modules, and type definitions. Table 7-1 shows these three kinds of containers and what they can contain. For completeness, the table includes type abbreviations, which are slightly different because you can't use them as a container for other constructs.

Table 7.1. Namespaces, Modules, Types, and What They Can Contain

Entity

Description

Examples

Namespace

A namespace can contain further namespaces, modules, and types. Multiple DLLs can contribute to the same namespace.

System,Microsoft.FSharp

Module

A module can contain nested modules, types, and values.

Microsoft.FSharp.Collections.Map, Microsoft.FSharp.Collections.List

Concrete type definition

A type definition can contain members and nested type definitions.

System.String, System.Int32

Type abbreviation

A type abbreviation such as string, for System.String, can't act as containers for additional members, values, or types.

int,string

Putting Your Code in a Namespace

You saw in Chapter 6 how to define type definitions with members and modules with values. Now, you look at how to place these in namespaces. Listing 7-6 shows a file that contains two type definitions, both located in the namespace Acme.Widgets.

Example 7.6. A File Containing Two Type Definitions in a Namespace

namespace Acme.Widgets

    type Wheel     = Square | Round | Triangle

    type Widget    = { id: int; wheels: Wheel list; size: string }

Namespaces are open, which means multiple source files and assemblies can contribute to the same namespace. For example, another implementation file or assembly can contain the definitions shown in Listing 7-7.

Example 7.7. A File Containing Two Type Definitions in Two Namespaces

namespace Acme.Widgets

    type Lever    = PlasticLever | WoodenLever

namespace Acme.Suppliers

    type LeverSupplier    = { name: string; leverKind: Acme.Widgets.Lever }

The file in Listing 7-7 contributes to two namespaces: Acme.Widgets and Acme.Suppliers. The two files can occur in the same DLL or different DLLs. Either way, when you reference the DLL(s), the namespace Acme.Widgets appears to have at least three type definitions (perhaps more, if other DLLs contribute further type definitions), and the namespace Acme.Suppliers has at least one.

Using Files as Modules

Chapter 6 introduced the notion of a module. In F#, a module is a simple container for values and type definitions. Modules are also often used to provide outer structure for fragments of F# code; many of the simple F# programs you've seen so far in this book have been in modules without your knowing. In particular:

  • Code fragments typed into F# Interactive and delimited by ;; are each implicitly placed in a module of their own.

  • Files compiled with the command-line compiler using Visual Studio or loaded into F# Interactive with #load have their values and types placed in a namespace or module according to the leading module or namespace declaration in the file. Declarations in files without a leading module or namespace declaration are placed in a module whose name is derived from the name of the implementation file.

Let's look at the second case in more detail. You can explicitly declare the name of the namespace/module for a file by using a leading module declaration. For example, Listing 7-8 defines the module Acme.Widgets.WidgetWheels, regardless of the name of the file containing the constructs.

Example 7.8. An Implementation Module with an Explicit Initial module Declaration

module Acme.Widgets.WidgetWheels

    let wheelCornerCount = Map.of_list [(Wheel.Square,   4);
                                        (Wheel.Triangle, 3);
                                        (Wheel.Round,    0);  ]

Here the first line gives the name for the module defined by the file. The namespace is Acme.Widgets, the module name is WidgetWheels, and the full path to the value is Acme.Widgets.WidgetWheels.wheelCornerCount.

Creating Assemblies, DLLs, and EXEs

This book has used F# Interactive for most of the samples. F# Interactive is excellent when you're exploring a set of libraries and writing scripts or small applications that use them. But to understand how to write libraries and organize other kinds of applications, you need to learn how to use the F# command-line compiler, fsc.exe, to compile your code into DLLs and EXEs. A Dynamic Link Library (DLL) is the Windows name for library components, and .exe is the extension used for executable programs.

As you saw at the start of this chapter, all .NET code exists in an assembly, which is, roughly speaking, either a DLL or an EXE. Assemblies can also contain supporting code and data files. Every time you compile a set of files using fsc.exe, you create one assembly: either a DLL or an EXE. Even when you use F# Interactive (fsi.exe), you're dynamically adding code to a dynamically generated DLL. You now learn how to use the command-line compiler to create DLLs and EXEs.

Compiling EXEs

To compile code to an EXE, you call fsc.exe with the names of your source code files in dependency order. For example, if the file dolphin.fs contains the code in Listing 7-9, then you can compile the code using fsc dolphin.fs. You can also use the -o flag to name the generated EXE.

Example 7.9. File dolphins.fs

let longBeaked = "Delphinus capensis"
let shortBeaked = "Delphinus delphis"
let dolphins = [ longBeaked; shortBeaked ]
printfn "Known Dolphins:  %A" dolphins

You can now compile this code to an EXE as shown here:

C:fsharp> fsc dolphins.fs
C:fsharp> dir dolphins.exe
...
05/04/2010  19:21             3,124 dolphins.exe

C:fsharp>dolphins.exe
Known Dolphins: ["Delphinus capensis"; "Delphinus delphis"]

Compiling DLLs

A DLL is a library containing compiled .NET or F# code (it can also contain native code with instructions and data dependent on particular machine architectures, such as Intel x86 and AMD x64). To compile F# code into a DLL, you take one or more source files and invoke fsc.exe with the -a option. For example, let's assume the file whales.fs contains the code shown in Listing 7-10. (This sample also includes some documentation comments, referred to in the "Generating Documentation" section later in this chapter.)

Example 7.10. File whales.fs

module Whales.Fictional

/// The three kinds of whales we cover in this release
type WhaleKind =
    | Blue
    | Killer
    | GreatWhale

/// The main whale
let moby = "Moby Dick, Pacific", GreatWhale

/// The backup whale
let bluey = "Blue, Southern Ocean", Blue

/// This whale is for experimental use only
let orca = "Orca, Pacific", Killer

/// The collected whales
let whales = [| moby; bluey; orca |]

You can now compile the code as follows:

C:	est> fsc -g -a whales.fs
C:	est> dir whales.dll
...
05/04/2010  19:18             6,656 whales.dll

Here you've added one command-line flag: -g to generate debug output. When compiling other assemblies, you need to reference your DLLs. For example, consider the code in Listing 7-11, which needs to reference whales.dll.

Example 7.11. File whalewatcher.fs

open Whales
open System

let idx = Int32.Parse(Environment.GetCommandLineArgs().[1])
let spotted = Fictional.whales.[idx]

printfn "You spotted %A!" spotted

You can compile this file by adding an -r flag to reference the DLLs on which the code depends. You can also use the -o flag to name the generated EXE or DLL and the -I flag to list search paths:

C:fsharp> fsc -g -r whales.dll -o watcher.exe whaleWatcher.fs
C:fsharp> dir watcher.exe
...
05/04/2010  19:25             3,584 watcher.exe

C:fsharp> watcher.exe 1
You spotted ("Blue, Southern Ocean", Blue)!

Note

.NET assemblies often contain a large amount of code. For example, a single assembly may contain as many as 50 or 100 F# source code files. Having many small assemblies may seem tempting, such as to improve compilation times during development, but can lead to difficulties when you're managing updates and can be confusing to end users. Large assemblies are easier to version: you have to update only one component, and you can package multiple updates into a single new version of the DLL.

Mixing Scripting and Compiled Code

Small programs are often used as both interactive scripts and as small compiled applications. Here are some useful facts to know about scripting with F# and F# Interactive:

  • Small F# scripts use the extension .fsx.

  • You can use the scripting directive #load from F# Interactive to load and compile one or more source code files as if they had been compiled using the command-line compiler.

  • You can load one script from another script by the F# Interactive directive #use. This is very much like #load except that the file being loaded may contain further scripting directives.

  • You can reference DLLs from within scripts by using the #r and #I directives, which act like the -r and -I directives of the F# command-line compiler.

  • You can access command-line arguments from within scripts by using the expression fsi.CommandLineArgs. Within compiled code, you should use System.Environment.GetCommandLineArgs. Within code used in both modes, you should use conditional compilation to switch between these, as shown in the next coding example.

  • You can run a script on startup by using the --exec command-line option for fsi.exe or by giving a single file name on the command line. You can find other command-line options by using fsi.exe --help.

Conditional compilation is a particularly useful feature for scripts—especially the predefined conditional compilation symbols COMPILED and INTERACTIVE. The former is set whenever you compile code using the command-line compiler, fsc.exe, and the latter is set whenever you load code interactively using F# Interactive. A common use for these flags is to start the GUI event loop for a Windows Forms or other graphical application, such as using System.Windows.Forms.Application.Run. F# Interactive starts an event loop automatically, so you require a call to this function in the compiled code only:

open System.Windows.Forms

let form = new Form(Width=400, Height=300,
                    Visible=true, Text="F# Forms Sample")
#if COMPILED
// Run the main code
System.Windows.Forms.Application.Run(form)
#endif

Note

You can specify additional conditional compilation directives by using the --define command-line compiler option.

Choosing Optimization Settings

The F# compiler comes with a simple choice of optimization levels. You nearly always want to compile your final code using --optimize, which applies maximum optimizations to your code. This is also the default optimization setting for fsc.exe.

The F# compiler is a cross-module, cross-assembly optimizing compiler, and it attaches optimization information to each assembly you create when using optimization. This information may contain some code fragments of your assembly, which may be inlined into later assemblies by the optimizing compiler. In some situations, you may not want this information included in your assembly. For example, you may expect to independently version assemblies, and in this case you may want to ensure that code is never duplicated from one assembly to another during compilation. In this case, you can use the --nooptimizationdata switch to prevent optimization data being recorded with the assemblies you create.

Generating Documentation

In Chapter 2, you saw that comments beginning with /// are XML documentation comments, which are used by interactive tools such as Visual Studio. They can also be collected to generate either HTML or XML documentation. You generate HTML documentation using an auxiliary tool such as FsHtmlDoc, available in the F# Power Pack.

You can also generate a simple XML documentation file using the --doc command-line option. You must name the output file. For example, using fsc -a --doc:whales.xml whales.fs for the code in Listing 7-10 generates the file whales.xml containing the following:

<?xml version="1.0" encoding="utf-8"?>
<doc>
    <assembly><name>whales</name></assembly>
    <members>
      <member name="T:Whales.Fictional.WhaleKind">
        <summary> The three kinds of whales we cover in this release</summary>
      </member>
      <member name="P:Whales.Fictional.bluey">
      <summary> The backup whale</summary>
      </member>
      <member name="P:Whales.Fictional.moby">
       <summary>The main whale</summary>
      </member>

      <member name="P:Whales.Fictional.orca">
       <summary> This whale is for experimental use only</summary>
      </member>
      <member name="P:Whales.Fictional.whales">
       <summary> The collected whales</summary>
      </member>
      <member name="T:Whales.Fictional">
      </member>
    </members>
</doc>

Building Shared Libraries and Using the Global Assembly Cache

You commonly need to share libraries among multiple applications. You can do this by using any of the following techniques:

  • Including the same library source file in multiple projects and/or compilations

  • Duplicating the DLL for the library into each application directory

  • Creating a strong name-shared library

This section covers the last option in more detail. A strong name-shared library has the following characteristics:

  • It's a DLL.

  • You install it in the .NET global assembly cache (GAC) on the target machine.

  • You give it a strong name by using --keyfile.

  • You package all of its supporting data files using --linkresource.

  • You (optionally) give it a version number using an AssemblyVersion attribute in your code.

  • You ensure that all of its dependencies are shared libraries.

The usual place to install shared libraries is the .NET GAC. The GAC is a collection of assemblies installed on the machine and available for use by any application that has sufficient privileges. Most libraries used in this book such as System.Windows.Forms.dll are installed in the GAC when you install the .NET Framework on a machine.

The remaining requirements are easy to satisfy and are conditions that must hold before you install something in the GAC. For example, assemblies must have strong names. All assemblies have names; for example, the assembly whales.dll (which you compiled in the earlier "Compiling DLLs" section using fsc -a whales.fs) has the name whales. An assembly with a strong name includes a hash using a cryptographic public/private key pair. This means only people who have access to the private key can create a strong-named assembly that matches the public key. Users of the DLL can verify that the contents of the DLL were generated by someone holding the private key. A strong name looks something like this:

mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

It's easy to create a strong-named assembly: generate a public/private key pair using the sn.exe tool that comes with the .NET Framework SDK, and give that as your keyfile argument. You can install libraries into the GAC using the .NET Framework SDK utility gacutil.exe. The following command-line session shows how to do this for the code shown in Listing 7-10:

C:UsersdsymeDesktop> sn.exe -k whales.snk
Microsoft (R) .NET Framework Strong Name Utility  Version 2.0.50727.42
Copyright (c) Microsoft Corporation. All rights reserved.

Key pair written to whales.snk

C:UsersdsymeDesktop> fsc -a --keyfile:whales.snk whales.fs

C:UsersdsymeDesktop> gacutil /i whales.dll
Microsoft (R) .NET Global Assembly Cache Utility. Version 2.0.50727.42
Copyright (c) Microsoft Corporation. All rights reserved.

Assembly successfully added to the cache

Installer generators such as WiX also include directives to install libraries into the GAC. Installer generators are discussed later in this chapter.

Note

If you're planning to write libraries for use by the rest of the world, we recommend that you take the time to read the .NET library design guidelines, document them using XML and HTML docs, and learn how to version your libraries. Chapter 19 takes a deeper look at guidelines for library design.

Using Static Linking

Sometimes applications use a DLL as a library; but when it comes to deploying the application on a website or as installed software, it may be easier to bundle that DLL as part of the application. You can do this in two ways: by placing the DLL alongside the EXE for the application or by statically linking the DLL when you create the EXE. You select the DLL by using the --staticlink compiler option with the assembly name of the DLL.

You can also bundle the F# libraries into your application to give a zero-dependency application. You statically link all DLLs that depend on the F# libraries by using the --standalone compiler option.

Static linking can cause problems in some situations and should only be used for a final EXE or a DLL used as an application plug-in.

Using Signature Types and Files

Every piece of F# code you write has a signature type. The inferred signature type for a piece of code is shown for every code fragment you enter into F# Interactive and can also be reported by using the F# command-line compiler, fsc.exe, with the -i option. For example, consider the following code, placed in an implementation file ticktock.fs:

module Acme.TickTock

    type TickTock = Tick | Tock

    let ticker x =
        match x with
        | Tick -> Tock
        | Tock -> Tick

You can now invoke the command-line compiler from a command prompt:

C:UsersdsymeDesktop> fsc -i -a ticktock.fs

namespace Acme

module TickTock =
    type TickTock = Tick | Tock
    val ticker : TickTock -> TickTock

The inferred signature shows the results of type inference and takes into account other information such as accessibility annotations.

Using Explicit Signature Types and Signature Files

If you want, you can make the inferred types explicit by using an explicit signature type for each implementation file. The syntax used for explicit signature types is identical to the inferred types reported by F# Interactive or fsc.exe, like those shown previously. If you use explicit signatures, they're placed in a signature file, and they list the names and types of all values and members that are accessible in some way to the outside world. Signature files should use the same root name as the matching implementation file with the extension .fsi. For example, Listing 7-12 shows the implementation file vector.fs and the explicit signature file vector.fsi.

Example 7.12. A Signature File vector.fsi with Implementation File vector.fs

namespace Acme.Collections
    type SparseVector =
        new: unit -> SparseVector
        member Add: int * float -> unit
        member Item : int -> float with get

namespace Acme.Collections
    open System.Collections.Generic
    type SparseVector() =
        let elems = new SortedDictionary<int,float>()
        member vec.Add(k,v) = elems.Add(k,v)
        member vec.Item
            with get i =
                if elems.ContainsKey(i) then elems.[i]
                else 0.0
            and  set i v =
                elems.[i] <- v

You can now invoke the command-line compiler from a command-line shell:

C:UsersdsymeDesktop> fsc vector.fsi vector.fs

Although signature files are optional, many programmers like using them, especially when writing a library framework, because the signature file gives a place to document and maintain a component's public interface. But there is a cost to this technique: signatures duplicate names and some other information found in the implementation.

Note

You can use signature types to hide constructs, which can be used as an alternative to giving accessibility annotations on types, values, and members. Neither accessibility annotations nor signatures can hide type abbreviations, for the reasons discussed in the earlier "Hiding Things with Accessibility Annotations" section. Some additional restrictions apply to both hiding techniques. For example, if a type is revealed to be a constructed class type or interface type, then all its abstract members must be included in the signature. Similarly, a record type must reveal all its fields, and a discriminated union type must reveal all its cases. Also, in some languages such as OCaml, a signature may restrict the type of an implementation construct. For example, if a function is inferred to have a generic type 'a -> 'a, then in OCaml the signature may specify a more restricted type such as int -> int. This isn't permitted in F#.

When Are Signature Types Checked?

Signature types are checked after an implementation has been fully processed. This is unlike type annotations that occur directly in an implementation file, which are applied before an implementation fragment is processed. This means the type information in a signature file isn't used as part of the type-inference process when processing the implementation.

Note

Each signature file must appear before its corresponding implementation file in the compilation order for an F# project. In Visual Studio, this means the signature file must come before the implementation file in the project listing. This is because the signature is required in order to check the contents of the implementation file after the implementation file is fully processed.

Packaging Applications

This section covers some of the more pragmatic issues involved in in designing applications and choosing how to package both your code and data. First, however, let's talk about the sorts of things you may be building with F#.

Packaging Different Kinds of Code

Table 7-2 lists some of the kinds of software implemented with F#. These tend to be organized in slightly different ways and tend to use encapsulation to varying degrees. For example, encapsulation is used heavily in frameworks but not when you're writing 100-line scripts.

Table 7.2. Some Kinds of Software Built Using F#

Software Entity

Description

Script

A program or set of program fragments, usually in a single file and less than 1,000 lines, usually with an .fsx extension, run through F# Interactive. Sometimes also compiled. Organized using functions and occasional type definitions. Freely uses static global state. Usually has no signature file or accessibility annotations.

Application

An EXE or a web application DLL, perhaps with some supporting DLLs. Organized using namespaces, modules, functions, and some abstract types. Often uses some static global state. Some internal files and data structures may have signatures, but often these aren't needed.

Application extension (plug-in or add-on)

A component that extends an application, often compiled as a DLL containing types along with an accompanying XML file that describes the plug-in to the application. The host application loads the DLLs using .NET reflection. Generally has no static state because this lets the application instantiate multiple instances of the plug-in. Example: the DLL plug-ins for Paint.NET, a popular .NET image-manipulation program.

Framework

A collection of related type definitions, functions, and algorithms organized according to established .NET and F# library-design guidelines. Usually compiled as a DLL, strong-name signed, installed into the GAC on the target machine, and versioned as an independent entity. Generally has no static state except where it mediates essential state on the host computer or operating system.

Framework extension

A component that extends a framework, usually by defining types that implement particular interfaces. Organized in an appropriate namespace as a simple set of classes and functions that generate objects that implement the interfaces defined in a framework. Generally has no static state. Example: the Firebird.NET API, which provides implementations of the ADO.NET Data Access framework interfaces to enable access to Firebird databases.

Using Data and Configuration Settings

So far, this book has focused on code. In reality, almost every program comes with additional data resources that form an intrinsic part of the application. Common examples of the latter include the resource strings, sounds, fonts, and images for GUI applications. Applications typically select between different data resources based on language or culture settings. Often, programs also access additional parameters, such as environment variables derived from the execution context or registry settings recording user-configuration options. It can be useful to understand the idioms used by .NET to make managing data and configuration settings more uniform. Table 7-3 shows some of the terminology used for data resources.

Table 7.3. Application Data: Terminology

Terminology

Meaning

Example

Static application data

A data resource whose name/location is always known, whose value doesn't change during execution, and that your application can generally assume always exists.

The PATH environment variable or a data file distributed with your application.

Strongly typed data

Data accessed as a named, typed .NET value through code written by you or generated by a tool you're using. The code hides the complexity of locating and decoding the resource.

An icon data resource decoded to a System.Drawing.Icon value. The ResGen.exe tool can generate strongly typed APIs for common Windows resources such as bitmaps and icons.

GUI resource

A string, a font, an icon, a bitmap, an image, a sound, or another binary resource that is attached to a Windows application or DLL using the Win32 .res format or .NET managed .resx format. Often dependent on language/culture settings.

A bitmap added as a resource to a Windows Forms application or an error message from a compiler.

You may need to access many different kinds of data resources and application settings. Table 7-4 summarizes the most common ones.

Table 7.4. Commonly Used Data and Configuration Settings

Data Resource

Notes

Source directory

The source directory containing the source file(s) at time of compilation. Often used to access further resources in F# Interactive scripts or for error reporting in compiled applications. Accessed using the __SOURCE_DIRECTORY__ predefined identifier.

Command arguments

Arguments passed to the invocation of the program. Accessed using System.Environment.GetCommandLineArgs and fsi.CommandLineArgs when running in F# Interactive.

Installation location

Where a program EXE or DLL is installed. Accessed by using System.Windows. Forms.Application.StartupPath or by reading the Assembly.Location of any of the types defined in your assembly. For F# Interactive scripts, the installation location is usually the same as the source directory.

User directories

Paths to common logical directories such as Program Files, My Documents, and Application Data. Accessed using System.Environment.GetFolderPath.

Environment variables

User- or machine-wide settings such as PATH and COMPUTERNAME. Accessed using System.Environment.GetEnvironmentVariable.

Registry settings

User- or machine-wide settings used to hold the vast majority of settings on a Windows machine. Accessed using Microsoft.Win32.Registry.GetValue and related types and methods.

Configuration settings

Database connection strings and other configuration settings, often used for web applications. If an application is called MyApp.exe, then this is usually stored in a file such as MyApp.exe.config alongside the executable; web applications use a Web.Config file. Accessed using System.Configuration.ConfigurationManager.

Isolated storage

A special storage area accessible only to an installed application and that looks just like disk storage.

Fonts, colors, icons, and so on

Specifications of Windows-related resources. Often taken from predefined system fonts and colors using the functions in System.Drawing: for example, Color.MidnightBlue or new Font(FontFamily.GenericMonospace, 8.0f). Can be added as a binary resource to an assembly and accessed using System.Resources.ResourceManager.

You can author GUI data resources such as fonts, images, strings, and colors by creating a .resx file using a tool like Visual Studio. You then compile them to binary resources by using the resgen.exe tool that is part of the .NET Framework SDK. Most development environments have tools for designing and creating these resources. Often, the .NET Framework contains a canonical type such as System.Drawing.Color for any particular kind of resource; you should avoid writing needless duplicate types to represent them.

Sometimes it's a good idea to make sure a data resource is officially part of an assembly. For example, this is required if the assembly will be installed into the GAC. You can embed resources in applications (or associate them with a DLL) by using the --resource compiler option.

For example, a set of samples called F# Samples101 on the Microsoft Code Gallery contains the following:

  • The source files samples.fs, sampleform.fs, and program.fs.

  • The .NET resource file SampleForm.resx, created and edited using the Visual Studio tools for designing icons. This file contains 57KB of XML data specifying, among other things, a default icon for the upper-left corner image on the Windows operating system and six images used by the application. These are held in an image stream under the XML key imageList.ImageStream.

The application is compiled using the following command line:

resgen SampleForm.resx
fsc.exe --resource:SampleForm.resource sample.fs sampleform.fs program.fs

In the application, the resources are accessed from the type SampleForm contained in the file sampleform.fs. The following code fragments occur in that file. The first creates a ComponentResourceManager that is ready to read resources from an assembly. The argument typeof<SampleForm> ensures that the resource manager reads resources from the assembly that contains the type SampleForm (that is, the type being defined). Chapter 9 discusses the typeof function in more detail:

open System.ComponentModel
let resources = new ComponentResourceManager(typeof<SampleForm>)

The following lines retrieve images from the resource stream:

open System.Windows.Forms
let imageList = new System.Windows.Forms.ImageList()
imageList.ImageStream <- (resources.GetObject("imageList.ImageStream")
                            :?> System.Windows.Forms.ImageListStreamer)
imageList.Images.SetKeyName(0, "Help")
imageList.Images.SetKeyName(1, "BookStack")
imageList.Images.SetKeyName(2, "BookClosed")
imageList.Images.SetKeyName(3, "BookOpen")
imageList.Images.SetKeyName(4, "Item")
imageList.Images.SetKeyName(5, "Run")

The images from the resource file are associated with graphical components using the ImageKey property of various Windows Forms controls. For example, the application's Run button is associated with the Run image stored in the resource file using the following lines:

let runButton = new Button(ImageAlign = ContentAlignment.MiddleRight,
                            ImageKey = "Run",
                            ImageList = imageList)

Note

.NET uses application configuration files (for static application settings) and isolated storage for settings private to an installed application for a particular user, including for applications downloaded from the Web. You can access isolated storage using the System.IO.IsolatedStorage namespace and access the Windows registry using the Microsoft.Win32.Registry namespace.

Summary

In this chapter, you learned about a number of issues related to encapsulating, organizing, and packaging code, from the raw language devices used for accessibility and encapsulation within code to building assemblies and the related topics of packaging applications, building DLLs, and including data resources as part of your libraries and applications.

The next chapter looks at some of the foundational techniques you need to master as you progress with learning to program in F#.

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

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