Tải bản đầy đủ (.pdf) (18 trang)

Organizing, Annotating, and Quoting Code

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (238.02 KB, 18 trang )

Organizing, Annotating, and
Quoting Code
A
n important part of any programming language is the ability to organize code into logical
chunks. It’s also important to be able to annotate code with notes about what it does, for
future maintainers and even yourself.
It has also become common to use attributes and data structures to annotate assemblies
and the types and values within them. Other libraries or the CLR can then interpret these
attributes. I cover this technique of marking functions and values with attributes in the sec-
tion “Attributes.” The technique of compiling code into data structures is known as
quoting,
and I cover it in the section “Quoted Code” toward the end of the chapter.
Modules
F# code is organized into modules, which are basically a way of grouping values and types
under a common name. This organization has an effect on the scope of identifiers. Inside a
module, identifiers can reference each other freely. To reference identifiers outside a module,
the identifier must be qualified with the module name, unless the module is explicitly opened
with the
open directive (see “Opening Namespaces and Modules” later in this chapter).
By default, each module is contained in a single source file. The entire contents of the
source file make up the module, and if the module isn’t explicitly named, it gets the name of
its sour
ce file, with the first letter capitalized. (F# is case sensitive, so it’s important to remem-
ber this.) It’s recommended that such
anonymous modules be used only for very simple
programs.
To explicitly name a module, use the keyword
module. The keyword has two modes of
operation. One gives the same name to the whole of the source file. The other mode gives a
name to a section of a source file; this way several modules can appear in a source file.
To include the entire contents of a source file in the same explicitly named module, you


must place the
module keyword at the top of the source file. A module name can contain dots,
and these separate the name into parts. For example:
module Strangelights.Foundations.ModuleDemo
You can define nested modules within the same source file. Nested module names
cannot contain dots. After the nested module’s name comes an equals sign followed by the
indented module definition. You can also use the keywords
begin and end. To wrap the
111
CHAPTER 6
■ ■ ■
7575Ch06.qxp 4/27/07 1:11 PM Page 111
module definition, you can nest submodules. The following code defines three submodules,
F
irstModule
, S
econdModule
,
and
T
hirdModule
. T
hirdModule
i
s nested within
S
econdModule
.
#light
module FirstModule =

let n = 1
module SecondModule =
let n = 2
module ThirdModule =
let n = 3
Note that different submodules can contain the same identifiers without any problems.
Modules affect the scope of an identifier. To access an identifier outside of its module, you
need to qualify it with the module name so there is no ambiguity between identifiers in differ-
ent modules. In the previous example, the identifier
n is defined in all three modules. The
following example shows how to access the identifier
n specific to each of the modules:
let x = FirstModule.n
let y = SecondModule.n
let z = SecondModule.ThirdModule.n
A module will be compiled into a .NET class, with the values becoming methods and
fields within that class. You can find more details about what an F# module looks like to other
.NET programming languages in “Calling F# Libraries from C#” in Chapter 13.
Namespaces
Namespaces help organize your code hierarchically. To help keep module names unique
across assemblies, the module name is qualified with a namespace name, which is just a char-
acter string with parts separated by dots. For example, F# provides a module named
List, and
the .NET BCL provides a class named
List. There is no name conflict, because the F# module
is in namespace
Microsoft.FSharp, and the BCL class is in namespace
System.Collections.Generic.
It’s important that namespace names be unique. The most popular convention is to start
namespace names with the name of a company or organization, followed by a specific name

that indicates functionality. Although it

s not obligator
y to do this
, the convention is so widely
followed that if you intend to distribute your code, especially in the form of a class library,
then y
ou should adopt it too.

Note
Interestingly, at the IL level, there is no real concept of namespaces. A name of a class or module is
just a long identifier that might or might not contain dots. Namespaces are implemented at the compiler
level. When you use an
open
directive, you tell the compiler to do some extra work; to qualify all your identi-
fiers with the given name,
if it needs to; and to see whether this results in a ma
tch with a value or type.
CHAPTER 6

ORGANIZING, ANNOTATING, AND QUOTING CODE
112
7575Ch06.qxp 4/27/07 1:11 PM Page 112
In the simplest case, you can place a module in a namespace simply by using a module
n
ame with dots in it. The module and namespace names will be the same. You can also explic-
itly define a namespace for a module with the
namespace directive. For example, you could
replace the following:
module Strangelights.Foundations.ModuleDemo

with this to get the same result:
namespace Strangelights.Foundations
module ModuleDemo
This might not be too useful for modules, but as noted in the previous section, submodules
names cannot contain dots, so you use the
namespace directive to place submodules within a
namespace. For example:
#light
namespace Strangelights.Foundations
module FirstModule =
let n = 1
module SecondModule =
let n = 2
module ThirdModule =
let n = 3
This means that once compiled to the outside world, the first instance of n will be accessi-
ble using the identifier
Strangelights.Foundation.FirstModule.n instead of just
FirstModule.n.
It’s possible to define a namespace without also using a
module directive, but then the
source file can contain only type definitions. For example:
#light
namespace Strangelights.Foundations
type MyRecord = { field : string }
The following example will not compile, because you can’t place a value definition
dir
ectly into a namespace without explicitly defining a module or submodule within the
namespace.
#light

namespace Strangelights.Foundations
let value = "val"
In fact, the namespace directive has some interesting and subtle effects on what your code
looks like to other languages; I cover this in “Calling F# Libraries from C#” in Chapter 13.
CHAPTER 6

ORGANIZING, ANNOTATING, AND QUOTING CODE
113
7575Ch06.qxp 4/27/07 1:11 PM Page 113
Opening Namespaces and Modules
A
s you have seen in the previous two sections, to specify a value or type that is not defined in
the current module, you must use its qualified name. This can quickly become tedious because
some qualified names can be quite long. Fortunately, F# provides the
open directive so you can
u
se simple names for types and values.
The
open keyword is followed by the name of the namespace or module you want to open.
For example, you could replace the following:
#light
System.Console.WriteLine("Hello world")
with this:
#light
open System
Console.WriteLine("Hello world")
Note that you don’t need to specify the whole namespace name. You can specify the front
part of it and use the remaining parts to qualify simple names. For example, you can specify
System.Collections rather than the namespace, System.Collections.Generic, and then use
Generic.List to create an instance of the generic List class, as follows:

#light
open System.Collections
let l = new Generic.Dictionary<string, int>()

Caution
The technique of using partially qualified names, such as
Generic.Dictionary
, can make
programs difficult to maintain. Either use the name and the full namespace or use just the name.
You can open F# modules, but you cannot open classes from non-F# libraries. If you open
a module, it means you can reference values and types within it by just their simple names.
F
or example, the follo
wing finds the length of an array and binds this to the identifier
len:
#light
open Microsoft.FSharp.MLLib.Array
let len = length [| 1 |]
Some argue that this ability to open modules directly should be used sparingly because it
can make it difficult to find where identifiers originated. In fact, modules can typically be
divided into two categories: those that are designed to be accessed using qualified names and
those that are designed to be opened directly. Most modules are designed to be accessed with
qualified names; a few, such as
Microsoft.FSharp.Core.Operators, are designed to be directly
opened. The next example shows the
using function from Operators being used directly:
CHAPTER 6

ORGANIZING, ANNOTATING, AND QUOTING CODE
114

7575Ch06.qxp 4/27/07 1:11 PM Page 114
#light
open System.IO
open Microsoft.FSharp.Core.Operators
using (File.AppendText("text.txt") ) (fun stream ->
stream.WriteLine("Hello World"))
This example is slightly bogus because the Microsoft.FSharp.Core.Operators is opened
by the compiler by default. I used this because modules designed to be opened directly are a
rarity since it’s almost always useful for anyone reading the code to know where the function
originated.
If you open two namespaces that contain modules or classes of the same name, it won’t
cause a compile error. You can even use values from the modules or classes with the same
name, as long as the names of the values are not the same. In Figure 6-1, the namespace
System is opened. It contains the class Array, and a module, Array, is also available in F#’s
libraries. In the figure you can see both static methods from BCL’s
Array class, all starting with
a capital letter, and values from F#’s
Array module, which start with a small letter.
Figure 6-1. Visual Studio’s IntelliSense
Giving Namespaces and Modules Aliases
It can sometimes be useful to give an alias to a namespace or module to avoid naming clashes.
This is useful when two modules share the same name and value with a common name and
can also be a convenient way of switching some of your code to use two different implementa-
tions of similar modules. It can be more common when using libraries not written in F#.
The syntax for this is the
module keyword followed by an identifier, then an equals sign,
and then the name of the namespace or module to be aliased.
The following example defines
GColl as the alias for the namespace System.Collections.Generic:
CHAPTER 6


ORGANIZING, ANNOTATING, AND QUOTING CODE
115
7575Ch06.qxp 4/27/07 1:11 PM Page 115
#light
module GColl = System.Collections.Generic
let l = new GColl.List<int>()
Signature Files
Signature files are a way of making function and value definitions private to a module. You’ve
already seen the syntax for the definition of a signature file in Chapter 2. It is the source that
the compiler generates when using the
–i switch. Any definitions that appear in a signature
file are public and can be accessed by anyone using the module. Any that are not in the signa-
ture file are private and can be used only inside the module itself. The typical way to create a
signature file is to generate it from the module source and then go through and erase any val-
ues and functions that you want to be private.
The signature file name must be the same as the name of the module with which it is
paired. It must have the extension
.fsi or .mli. You must specify the signature file to the com-
piler. On the command line, you must give it directly before the source file for its module. In
Visual Studio, the signature file must appear before the source file in Solution Explorer.
For example, if you have the following in the file
Lib.fs:
#light
let funkyFunction x =
x + ": keep it funky!"
let notSoFunkyFunction x = x + 1
and you want to create a library that exposes funkyFunction but not notSoFunkyFunction, you
would use the signature code like this:
val funkyFunction : string -> string

and would use the command line like this:
fsc -a Lib.fsi Lib.fs
which results in an assembly named Lib.dll with one class named Lib, one public function
named
funkyFunction, and one private function named notSoFunkyFunction.
Module Scope
The order that
modules ar
e passed to the compiler is important, because it affects the scope of
identifiers within the modules and the order in which the modules are executed. I cover scope
in this section and execution order in the next.
V
alues and types within a module cannot be seen from another module unless the mod-
ule they’re in appears on the command line before the module that refers to them. This is
probably easier to understand with an example. Suppose you have a source file,
ModuleOne.fs,
containing the follo
wing:
CHAPTER 6

ORGANIZING, ANNOTATING, AND QUOTING CODE
116
7575Ch06.qxp 4/27/07 1:11 PM Page 116
#light
module ModuleOne
let text = "some text"
a
nd another module,
M
oduleTwo.fs

,
containing the following:
#light
module ModuleTwo
print_endline ModuleOne.text
These two modules can be compiled successfully with the following:
fsc ModuleOne.fs ModuleTwo.fs -o ModuleScope.exe
But the following command:
fsc ModuleTwo.fs ModuleOne.fs -o ModuleScope.exe
would result in this error message:
ModuleTwo.fs(3,17): error: FS0039: The namespace or module 'ModuleOne' is not
defined.
This is because ModuleOne is used in the definition of ModuleTwo, so ModuleOne must appear
before
ModuleTwo in the command line, or else ModuleOne will not be in scope for ModuleTwo.
Visual Studio users should note that the order in which files appear in Solution Explorer is
the order that they are passed to the compiler. This means it is sometimes necessary to spend
a few moments rearranging the order of the files when adding a new file to a project.
Module Execution
Roughly speaking, execution in F# starts at the top of a module and works its way down to
the bottom. Any values that are functions are calculated, and any top-level statements are
executed. So, the following:
module ModuleOne
print_endline "This is the first line"
print_endline "This is the second"
let file =
let temp = new System.IO.FileInfo("test.txt") in
Printf.printf "File exists: %b\r\n" temp.Exists;
temp
will give the following result:

CHAPTER 6

ORGANIZING, ANNOTATING, AND QUOTING CODE
117
7575Ch06.qxp 4/27/07 1:11 PM Page 117

×