<< Prev | - Up - | Next >> |
Chapter 1 used a rather simple application as example. This chapter shows how to use functors for the modular development of larger applications.
Principles of good software engineering suggest that larger applications should be designed and assembled from a collection of smaller modules. In Oz, this decomposition can be realized in terms of several functor definitions.
The primary purpose of a functor is to compute a module: It takes modules as input and computes a new module as output. As we have seen already, the import
section of a functor specifies its inputs as a list of module names. In addition, functors may also have an export
section which is basically a list of feature/value pairs that describes the module computed by the functor.
As demonstrated in Section 1.5 an application is run by executing the root functor. In our particular example, the root functor was rather simple in that it only imported system modules. However, larger applications will typically import modules computed by other application functors.
In the following we will build a trivial flight booking system featuring three components:
A data base server: It maintains a data base that contains available flights, where each flight has a unique id by which it can be identified. At first, the data base is not even persistent, but as we incrementally refine and improve our application, the data base evolves into a persistent and distributed data base server.
A graphical flight booking form, where a travel-minded user can choose a flight, enter her name, her E-mail address and so on. Later we will show how to build a web-based interface serving the same purpose.
The main component of our application that manages user requests to the data base and sets up the application.
All components are programmed as functors.
Let us start with the data base, which is the most straightforward part of our application. The data will be held in a dictionary that uses integers as keys, and arbitrary data structures as entries. The functor definition resides in file DB.oz
and its toplevel structure is as follows:
functor
<Export specification for DB.oz>
<Body for DB.oz>
end
The functor has no import specification, and its export specification is as follows:
export
add: Add
get: Get
getAll: GetAll
remove: Remove
The specification determines that the functor's module provides the features add
, get
, getAll
, and remove
, where the value of each feature is given by the variable after the following colon. The values of these variables are then computed by the functor's body.
For convenience, the export specification above may also be written more succinctly as follows:
export
Add
Get
GetAll
Remove
The shortcut to just use a variable identifier starting with a capital letter, defines both the variable identifier as well as the feature. The feature is the variable identifier with its first character changed to lowercase.
The functor body is of less importance to us here, however, you can find it in Section A.1. One advantage of modular program development is that during the design of an application one may concentrate first on finding the right interfaces, and only then provide corresponding implementations.
Even though the functor does not import any module, it uses predefined procedures (for example, Dictionary.new
to create a new dictionary). The compiler provides a set of variable identifiers, that refer to the basic operations on all primitive Oz data types. This set of identifiers is known as the base environment and is documented in detail in ``The Oz Base Environment''.
When a functor definition is compiled, all free variable identifiers must be bound by the base environment.
The functor that implements the graphical form to book flights has the following structure, and its definition resides in file Form.oz
:
functor
import
Tk
export
Book
define
proc {Book Fs ?Get}
%% Takes a list of flights and returns the booked flight
%% and information on the booking user
<Implementation of Book>
end
end
The root functor for our last minute flights application uses the previously defined functors that maintain the data base and that provide the user form. The root functor's definition resides in file LMF.oz
:
functor
import
DB Form % User defined
System Application % System
define
%% Enter some flights
{ForAll
<Sample flights> DB.add}
%% Book until all flights sold out
proc {Book}
case {DB.getAll}
of nil then
{System.showInfo 'All flights sold.'}
[] Fs then
O={Form.book Fs}
in
{System.showInfo ('Booked: '#O.key#
' for: '#O.first#
' '#O.last#
' ('#O.email#')')}
{DB.remove O.key}
{Book}
end
end
{Book}
{Application.exit 0}
end
Functors also are compilation units. Each functor definition is compiled separately. For our example, the following sequence of commands
ozc -c DB.oz -o DB.ozf
ozc -c Form.oz -o Form.ozf
ozc -c LMF.oz -o LMF.oza
compiles our example functor definitions. If you now change the functor definition in, say, DB.oz
but the interface of the created functor remains the same, none of the other functor definitions need recompilation.
Note that we have chosen as file extensions for pickled functors that are not supposed to be run as applications the string ozf
. For the root functor of our application we chose oza
. This is completely transparent as it comes to the semantics of our program, it is just a convention that makes it easier to tell apart which pickled functors are root functors of applications.
As before, we just execute the root functor of our application by applying the ozengine
command to LMF.oza
:
ozengine LMF.oza
The next chapter (Chapter 5) explains how applications that consist of several functors are executed.
<< Prev | - Up - | Next >> |