Version 1 of BOOP

Updated 2003-01-30 06:48:29

# BOOP stands for "basic object oriented programming" -- this is a minimal object oriented

    # helper that gives you tcl objects, member functions, object-local storage, memory cleanup
    # all with one tiny ::boop function of less than 100 lines of tcl.
    #
    # The aim of BOOP is to provide very simple object oriented programming helper in Tcl,
    # without doing anything fancy or complicated, that requires a learning curve or causes
    # other problems. If you want a full OOP Tcl environment, go for "incr Tcl" or "stoop".
    #
    # I tried to use stoop, but ran into a number of problems, namely: 1) it clashes badly
    # with the TclPro debugger, 2) member functions seem to be wrapped in a silent catch{}
    # statement, making debugging buggy member functions very difficult. Incr Tcl was too huge
    # for my needs, and I wanted all Tcl-solution, with a minimal learning curve for my coworkers.
    # I didn't want write OOPy Tcl code that no-one else would understand.
    #
    # In short, I wanted a Tcl OOP helper to be as simple, transparent as possible, and not to muck
    # with built in commands or cause problems with the TclPro debugger or confuse people reading
    # my OOPy code.
    #
    #
    # HOW TO USE BOOP
    #
    # Define functions in a namespace, like so:
    #
    #   namespace eval test {}
    #
    #   proc test::example {this arg} {
    #       incr ::($this,z)
    #       puts "I am $this and was passed '$arg' and z is '$::($this,z)'"
    #   }
    #
    #
    # If you want to initialize some member variables, you can do it in an init function, but
    # this is optional (the namespace's init function is called automatically at object construction time)
    # and the objectid is passed in "this". You can also optionally create a deinit function:
    #
    #   proc test::init {this} {
    #       puts "initializing $this"
    #       set ::($this,z) 0
    #   }
    #
    #   proc test::deinit {this} {
    #       puts "deinitializing $this"
    #   }
    #
    # Note how variables for the object are stored in the ::($this,varname) global array. If you need
    # to store arrays in your object, use "array get/set" to convert to/from a list.
    # 
    # Next, create your object with the ::boop command, passing the namespace name, like so:
    #
    #    set myobject [boop test]
    #
    # If your namespace has a namespace::init function it is called automatically by BOOP at this point.
    #
    # Then, just use the proc name (w/o the namespace name) as the 1st parameter, using the
    # objectid as the proc name, like so:
    #
    #    $myobject example "argument1"
    #    $myobject example "argument2"
    #
    # BOOP will automatically call your namespace::functionname with the objectid as the first parameter, so
    # be sure that all your member functions take "this" as their first parameter.
    #
    # Finally, be a good citizen and clean up your object with the "delete" member function,
    # which will first call your namespace::deinit function (if one exists) and then will
    # clean up the object, any variables in the global unnamed array with ($objectid*) in the
    # name, and the namespace.  Call delete like so:
    #
    #    $myobject delete
    #
    #
    # All in all, a complete example looks like this (bear in mind that init and deinit are optional):
    # 
    #   namespace eval test {}
    # 
    #   proc test::init {this} {
    #       puts "initializing $this"
    #       set ::($this,z) 0
    #   }
    # 
    #   proc test::deinit {this} {
    #       puts "deinitializing $this"
    #   }
    # 
    #   proc test::example {this arg} {
    #       incr ::($this,z)
    #       puts "I am $this and was passed '$arg' and z is '$::($this,z)'"
    #   }
    # 
    #   set myobject [boop test]
    #   $myobject example "argument1"
    #   $myobject example "argument2"
    #   $myobject delete
    #
    # Running this code with yield this screen output:
    #
    #   initializing ::test_1
    #   I am ::test_1 and was passed 'argument1' and z is '1'
    #   I am ::test_1 and was passed 'argument2' and z is '2'
    #   deinitializing ::test_1
    #
    ###########
    #
    # This Boop version alpha .1, released 1/29/2003, copyright 2003 John Buckman <[email protected]>.
    #
    # This source code is released under the GNU general public license.
    #
    ###########

    proc ::boop {class} {

        # make a number variable for this 
        if {![info exists ${class}::boop_number]} {
            namespace eval ${class} { variable boop_number 0 }
        }

        # keep track of the number of the object, so we don't duplicate
        variable ${class}::boop_number

        # increment the number of the object
        incr ${class}::boop_number

        # make a namespace for this object, so that member functions can store
        # variables in the namespace if they want to (need to make examples of this for docs)
        set namespacename "::${class}_$boop_number"
        namespace eval $namespacename {}

        # make a command based on this name of the object, and a delete member function 
        set helper " \
    proc $namespacename {args} { \n\
        set function \[lindex \$args 0\] \n\
        set args \[lreplace \$args 0 0\] \n\
        set newfunction \[concat ::${class}::\${function} ${namespacename} \$args\] \n\
        return \[eval \$newfunction\]
    } \n\

    # delete member function for the object  
    proc ${class}::delete {this} { \n \
        \
        # call the deinit function if it exists \n \
        set deinitfunction ::${class}::deinit \n \
        if {\[info procs \$deinitfunction\] != \"\"} { \n \
            eval \[list \$deinitfunction \$this\] \n \
        } \n \
        \
        # clear the un-named array of all names that start with this object name
        array unset {::} \"\${this}*\"
        \
        # delete the name space, in case it was used for anything
        namespace delete ::\$this \n \
        \
        # remove the object command
        rename \$this {} \n \
    } \
        "

        eval $helper

        set initfunction ${class}::init
        if {[info procs $initfunction] != ""} {
            eval [list $initfunction $namespacename]
        }

        # return the object id
        return $namespacename
    }