Version 1 of How to make a Tcl application - part two

Updated 2003-12-04 07:24:50

Arjen Markus This is the second part of the tutorial - it is not ideal to split the whole thing, but I do not want to run the risk that my Internet browser does not allow me to edit the page anymore because it is too large ;)


5. A library example

The second example we are going to examine is a small mathematical library, that is, a set of related procedures that do something more or less useful and that we want to be able to use in various applications. Such libraries can be small or extensive, coherent or simply a collection of lots of procedures that can be useful at times.

For a library to be useful, it is important to have:

  • A user manual, i.e. a description of what the library is meant for

and how to use its contents, but from a programmer's point of view.

  • A set of test cases, so that we can check that the library is

working as expected and will continue to do so if we change something.

We also need to take care that we do not define procedures that an application might also define for itself (so-called name clashes) or that global variables are kept out of each others' way.

To avoid problems we will use namespaces: our procedures and our variables "live" in a different world than the application that will use the library. The application can choose to import procedures from the library, but does not need to. For instance: our library contains a procedure "add", but an application can define its own procedure "add", as long as it does not import ours. (More on namespaces: Will Duquette's tutorial)

Here is the code to create the namespace (::Poly - the double colons indicate the namespace is located directly under the global namespace):

   namespace eval ::Poly {
      variable pcount   0
      variable pcoeffs

      namespace export polynomial add deriv
   }

We define a few namespace variables - they exist, just like global variables outside any procedure. By means of the command "namespace export" we identify the commands that are useful for an application and that can be imported (via a command "namespace import ::Poly::*" for instance).

To use this library in our applications, we have two choices:

  • The application sources the files containing the code directly
  • We provide the library as a genuine package

The latter is preferable, because then we do not need to know too much about the way the library is implemented - this is taken care of by the package mechanism.

How does this mechanism work? Well, by using a "package provide" statement:

   package provide Polynomials 1.0
   namespace eval ::Poly {
      ...
   }

(Note: the name of the package is independent of the namespace it lives in. Normally you would choose them equal. The number 1.0 is the version number)

Now, this is the first part. When the application asks for the package, via:

   package require Polynomials

Tcl will search through all directories listed in the variable auto_path to see if there is such a package. This process depends on the presence of a file called "pkgIndex.tcl" that contains information about the packages available in the directory.

As a developer of the library, you can use commands to generate this file for you, but in simple cases, it is easy enough to make it yourself:

   package ifneeded Polynomials 1.0 [list source [file join $dir poly.tcl]]

The variable "dir" is filled in during the directory search done by Tcl and enables you to keep any specific directory out of the file.

What remains is:

  • Store the source file (poly.tcl) and the package index (pkgIndex.tcl) in a suitable directory (check the auto_path variable for this or extend it yourself in your application)
  • Have your application "require" the package

Here is the code for our little package (this is actual working code!)

 # poly.tcl --
 #    Library for defining and manipulating polynomials
 #
 package provide Polynomials 1.0

 # Poly --
 #    Namespace for the procedures handling polynomials
 #    Variables:
 #    pcount  - counts the polynomials that have created,
 #              used to construct a unique name
 #    pcoeffs - coefficients of each polynomial
 #
 #

 namespace eval ::Poly {
    variable pcount   0
    variable pcoeffs

    namespace export polynomial add deriv
 }

 # EvalPolynomial --
 #    Private procedure to actually evaluate the given polynomial
 #    for a given argument
 # Arguments:
 #    pname     Name of the polynomial
 #    x         Value of the variable
 # Result:
 #    Value of the polynomial at the given variable
 #
 proc ::Poly::EvalPolynomial { pname x } {
    variable pcoeffs

    set result 0.0
    set powx   1.0
    foreach c $pcoeffs($pname) {
       set result [expr {$result+$c*$powx}]
       set powx   [expr {$powx*$x}]
    }
    return $result
 }

 # polynomial --
 #    Create a procedure that can evaluate the given polynomial
 # Arguments:
 #    coeffs    List of coefficients
 # Result:
 #    Name of new procedure
 #
 proc ::Poly::polynomial { coeffs } {
    variable pcount
    variable pcoeffs

    #
    # Create a unique ID for the new procedure
    # Careful - we will create the new procedure in the _current_
    # namespace
    #
    incr pcount
    set pname  "[namespace current]::P$pcount"

    #
    # Store the coefficients under the name of the new procedure
    # (easier access)
    #
    set pcoeffs($pname) $coeffs

    #
    # Create the procedure - yes, we can do that from within
    # a procedure: "proc" is just a command like any others!
    # (Note: we need to be careful with the special
    # characters though - the name "$pname" must be filled in
    #
    proc $pname { x } "return \[EvalPolynomial $pname \$x\]"

    #
    #
    return $pname
 }

 # add --
 #    Add two polynomials to give a new one
 # Arguments:
 #    p1        First polynomial (name of)
 #    p2        Second polynomial (name of)
 # Result:
 #    Name of new procedure implementing the sum
 #
 proc ::Poly::add { p1 p2 } {
    variable pcoeffs

    #
    # Get the coefficients of each
    #
    set pc1 $pcoeffs($p1)
    set pc2 $pcoeffs($p2)

    #
    # As the polynomials do not necessarily have the same
    # degree, first make the lists equal in length
    #
    # Note:
    # We use a clever trick to avoid too much and unnecessary
    # checking: at most one of the for-loops is actually
    # run.
    #
    set ncoeffs1 [llength $pc1]
    set ncoeffs2 [llength $pc2]

    for { set i $ncoeffs1 } { $i < $ncoeffs2 } { incr i } {
       lappend pc1 0.0
    }

    for { set i $ncoeffs2 } { $i < $ncoeffs1 } { incr i } {
       lappend pc2 0.0
    }

    #
    # Now we can easily loop over the two sets of coefficients
    # - they have the same length
    #
    set sumcoeffs {}

    foreach c1 $pc1 c2 $pc2 {
       lappend sumcoeffs [expr {$c1+$c2}]
    }

    #
    # Create the new polynomial, and return its name
    #
    return [polynomial $sumcoeffs]
 }

 # deriv --
 #    Determine the derivative of a polynomial
 # Arguments:
 #    pname     First polynomial (name of)
 # Result:
 #    Name of new procedure implementing the derivative
 #
 proc ::Poly::deriv { pname } {
    variable pcoeffs

    #
    # Get the coefficients
    #
    set pc $pcoeffs($pname)

    #
    # Loop over the coefficients: the first disappears
    # Note:
    # The command [lrange ...] is executed only once
    #
    set derivcoeffs {}

    set degree 1
    foreach c [lrange $pc 1 end] {
       lappend derivcoeffs [expr {$c*$degree}]
       incr degree
    }

    #
    # Create the new polynomial, and return its name
    #
    return [polynomial $derivcoeffs]
 }

 #
 # Some test code
 #
 if { [file tail $::argv0] == [info script] } {
    set p  [::Poly::polynomial {1 1 1}]
    set q  [::Poly::polynomial {-1 -1 1}]
    set p' [::Poly::deriv $p]
    set pq [::Poly::add $p $q]
    puts "Eval  = Actual      - Expected"
    puts "p(1)  = [$p 1]      - 3.0"
    puts "p(2)  = [$p 2]      - 7.0"
    puts "p(3)  = [$p 3]      - 13.0"
    puts "p'(1) = [${p'} 1]   - 3.0"
    puts "p'(2) = [${p'} 2]   - 5.0"
    puts "p'(3) = [${p'} 3]   - 7.0"
    puts "pq(1) = [$pq 1]     - 2.0"
    puts "pq(2) = [$pq 2]     - 8.0"
    puts "pq(3) = [$pq 3]     - 18.0"
 }

At the end you see a clever little trick:

 if { [file tail $::argv0] == [info script] } {
    ...
 }

The global variable argv0 holds the full name (including the directory) of the script that is being executed at start-up by tclsh or wish. The command [info script] returns the name of the script file that is being sourced.

So, if these two file names match, then we can be (almost) sure that the library's source file is being run standalone - a perfect opportunity for running a few tests.

TODO/Explain:

  • Coefficients in array
  • Three procedures for creating a polynomial
  • Extra comments
  • Style guide Ray Johnson, Will Duquette
  • expr and {}
  • tcltest
  • several source files

[ Category Tutorial ]