TIP 524: Custom Definition Dialects for TclOO

Login
Author:         Donal K. Fellows <[email protected]>
State:          Final
Type:           Project
Vote:           Done
Created:        23-Oct-2018
Post-History:
Keywords:       Tcl, object orientation, customization
Tcl-Version:    8.7
Tcl-Branch:     tip-524
Vote-Summary:   Accepted 2/0/4
Votes-For:      DKF, JN
Votes-Against:  none
Votes-Present:  BG, KBK, FV, SL

Abstract

This TIP proposes a mechanism for allowing classes to control how their instances define themselves.

Rationale

Though TclOO has a powerful mechanism for handling definitions (essentially based on executing Tcl scripts in a namespace that has the definition commands, via a mechanism very similar to namespace eval) it is not simple for user code to extend those mechanisms in a way that is specific to a particular set of classes. The oo::dialect package in Tcllib provides ways of doing this, but is internally rather complex; I believe the process should be much simpler.

Proposal

Every class will gain two internal fields that describe what namespace will be used to configure the instances of that class (i.e., what namespace to use in the namespace eval). One of those fields will be used when the instance of that class is being configured via oo::define and the other will be used when the instance is being configured va oo::define (or self within oo::define). When looking up what namespace to use, the inheritance hierarchy will be used to find the first class in the resolution order that defines a namespace; by default oo::object will provide the namespace ::oo::objdefine for one of the fields and oo::class will provide ::oo::define for the other, these being the current namespaces used for definitions.

There will a new class definition for exerting control over these fields:

definitionnamespace ?kind? namespace

This allows the current class to have its definition namespace for kind set to namespace (or the empty string). The kind must be -class (the default) or -instance to set the definition namespace for oo::define and oo::objdefine respectively. The root classes, oo::object and oo::class, cannot have their definition namespaces modified.

There will be a new introspection command for reading what the current definition namespace is for a class:

info class definitionnamespace class ?kind?

This will return from class the name of the current definition namespace of kind kind (must be either -class or -instance; -class is the default) that is set for class. If class does not specify one, the empty string is returned.

Usage Example

Effective use of this feature depends on using namespace path.

namespace eval mydialect {
    namespace path ::oo::define

    proc example {x} {
        puts "This is an example($x) in [uplevel 1 self]"
        method example {} [list puts example=$x]
    }
    proc constructor {arguments body} {
        set body [string cat {puts "This is [self]";} $body]
        tailcall ::oo::define constructor $arguments $body
    }
}

puts "<<POINT-1>>"

oo::class create metaclass {
    superclass oo::class
    definitionnamespace mydialect
}

puts "<<POINT-2>>"

metaclass create cls {
    constructor {abc} {
        puts "abc = $abc"
    }
    example 123
}

puts "<<POINT-3>>"
set obj [cls create gorp xyz]
puts "<<POINT-4>>"
puts $obj
$obj example

Which will print:

<<POINT-1>>
<<POINT-2>>
This is an example(123) in ::metaclass
<<POINT-3>>
This is ::gorp
abc = xyz
<<POINT-4>>
::gorp
example=123

This second example (adapted from the proposed manual page) shows just how thoroughly the ability to control the definition language can change things.

namespace eval myDefinitions {
    # Delegate to existing definitions where not overridden
    namespace path ::oo::define

    # A custom type of method
    proc exprmethod {name arguments body} {
        tailcall method $name $arguments [list expr $body]
    }

    # A custom way of building a constructor
    proc parameters args {
        uplevel 1 [list variable {*}$args]
        set body [join [lmap a $args {
            string map [list %VAR% $a] {
                set [my varname %VAR%] [expr {double($%VAR%)}]
            }
        }] ";"]
        tailcall constructor $args $body
    }
}

# Bind the namespace into a (very simple) metaclass for use
oo::class create exprclass {
    superclass oo::class
    definitionnamespace myDefinitions
}

# Use the custom definitions
exprclass create quadratic {
    parameters a b c

    exprmethod evaluate {x} {
        ($a * $x**2) + ($b * $x) + $c
    }
}
exprclass create cubic {
    parameters a b c d

    exprmethod evaluate {x} {
        ($a * $x**3) + ($b * $x**2) + ($c * $x) + $d
    }
}

# Showing the resulting classes and objects in action
quadratic create quad 1 2 3
for {set x 0} {$x <= 4} {incr x} {
    puts [format "quad(%d) = %.2f" $x [quad evaluate $x]]
}

cubic create cub 3 1 0.5 0.25
for {set x 0} {$x <= 4} {incr x} {
    puts [format "cub(%d) = %.2f" $x [cub evaluate $x]]
}

This prints:

quad(0) = 3.00
quad(1) = 6.00
quad(2) = 11.00
quad(3) = 18.00
quad(4) = 27.00
cub(0) = 0.25
cub(1) = 4.75
cub(2) = 29.25
cub(3) = 91.75
cub(4) = 210.25

Implementation

See the branch tip-524.

Copyright

This document has been placed in the public domain.