TIP 558: Basic Configure Support for TclOO

Login
Bounty program for improvements to Tcl and certain Tcl packages.
    Author:         Donal K. Fellows <donal.k.fellows@manchester.ac.uk>
    State:          Draft
    Type:           Project
    Vote:           Pending
    Created:        22-Dec-2019
    Post-History:
    Tcl-Version:    8.7
    Keywords:       Tcl, TclOO, properties
    Tcl-Branch:     tip-558

Abstract

This TIP proposes a basic mechanism for implementing a configure method suitable for implementing simple properties such as are found in the chan configure command (while also supporting inheritance). It does not provide the mechanism needed for the more sophisticated configure/cget of a Tk widget.

Rationale and Design Considerations

A common requirement for user objects that work like existing Tcl subsystems is that they support a way of being configured. There are many ways that such configuration systems can work, but the two most well known are probably:

  1. The channel configuration system (as in chan configure/fconfigure).

  2. Tk widgets.

Long term, we want to support both methods of working; that means that we will need new classes to introduce the capabilities required. The former can be a "configurable" class, and the latter can be a "widget" base class (and which is Out Of Scope for this TIP other than to note that this TIP is not attempting to support it; see TIP #560 for the detail of a proposal for dealing with objects with the Tk configuration style.).

Channel configuration is relatively simple, in that no attempt is ever made to make changes transactional, and one command (or one public method) handles all access. Each configurable property of a channel can be read-only, read-write or write-only; most properties are read-write but some are not (e.g., the -ttycontrol option of serial channels is write only, and the -error option of socket channels). If we provide fundamental mechanism for getting the list of all readable and writable properties (read-write properties would appear in both) while taking into account the inheritance graph, then we can use those lists to also do things like short form expansion and general error message generation.

Objects that provide a property must have the ability to run code in response to being asked to read or write (as appropriate) that property. This could theoretically be done through appropriate traces on variables, but I think it is easier to implement properties by calling (presumably unexported) methods that can read and write whatever internal state is required, as well as doing any validation of the provided value required for settable properties (with the good side-effect of meaning that custom error messages are trivial to implement).

Specification

This specification is split into two parts. The first is the part in C that all classes and objects will support, but which no pre-existing class (before this TIP) does anything with by default even once this TIP is accepted.

The second part of this specification is a purely scripted implementation of a simple property system based on that core that provides a property definition for creating properties on classes and objects, and a configure method for accessing them. It is intended that other types of property system will exist (especially to support Tk megawidgets) but such alternate systems are out of the scope of this TIP.

Basic Support for Properties

Classes and objects will gain two extra slots (see TIP #380) each, being the list of readable properties and the list of writable properties of the class or object. (These slots represent sets; just as with variable slots, the order of the elements in the slots will not be important, but a uniqueness constraint will be enforced so that no element can be in a slot twice.) The slot configuration commands will be placed in the namespaces ::oo::configuresupport (which is otherwise just an ordinary namespace) as they are not intended for ordinary user code to use directly: they will be:

  • readableproperties (i.e., ::oo::configuresupport::readableproperties) for configuring the slot listing readable properties of a class.
  • writableproperties for configuring the slot listing writable properties of a class.
  • objreadableproperties for configuring the slot listing readable properties of an instance.
  • objwritableproperties for configuring the slot listing writable properties of an instance.

The slots enforce uniqueness of property names within the slot (by removing duplicates), but otherwise impose no policy on what names are acceptable or define any semantics of what a property actually is or how it is manipulated beyond the discovery mechanism below. Order within these slots is not considered to be significant.

There will be also related info subcommands:

  • info class properties cls ?options...?

    This will report the properties for a (prototypical instance of the) class given by cls, reporting the readable ones by default, and the writable properties if the option -writable is given (and the option -readable may be given instead to explicitly get the default behaviour). By default it will report only the properties registered on the current class; the ones on the full class hierarchy up to the class are obtained by passing the option -all.

    The reported list will be sorted, as if by lsort -ascii -unique.

  • info object properties obj ?options...?

    This is similar, but instead applies to a particular object, obj. It may include properties set on just the object instance.

It should be noted here that the presence of a property in one class or instance does not prevent it from being listed by another class or instance, and that the listing of a property name in either slot does not actually guarantee that the property will be handled by the class that declares it; it is merely convention that it does so. There is also no way for a class or instance to prevent a property from being exposed once a superclass has decided to expose it.

To restate: this core mechanism is just an efficient property publication and discovery mechanism. It does not define how a property actually works. That is up to scripted classes that build on this.

Interface for Ordinary Scripts

Making use of these basic capabilities (themselves intended to be usable with other configuration mechanisms out of the scope of this TIP or entirely) will be a new metaclass, oo::configurable, which will provide the implementation of the configure method (the only method it provides) and (via TIP #524 capabilities) the user visible property declarations. This class will be written in pure Tcl.

As oo::configurable is a metaclass, it is used to create configurable classes and not just as a superclass/mixin. The instances of the classes it creates will be the actually configurable entities. A class should only inherit from oo::configurable when it wishes to be a metaclass that makes classes that have configurable properties on their instances.

Configure Method

The configure method provided to classes created by oo::configurable will support three modes of operation:

  1. obj configure

    This returns a dictionary of all readable properties from the obj instance, its class and superclasses and all mixins, and their values.

  2. obj configure -prop

    This returns the value of readable property -prop.

  3. obj configure -prop value ?-prop value...?

    This updates each writable property prop with its associated value, value, in the order given.

In all cases, the name -prop can be abbreviated provided it is unambiguous. The definitive list of property names is that provided by asking info object properties with the -all option and the appropriate mode flag. The configure method will handle the disambiguation.

When configure determines that it wishes to read a property, -prop, it delegates the actual reading of the property to a method, <ReadProp-prop> (e.g., for a property -foo, the method will be <ReadProp-foo>) which is assumed to take no extra arguments and return the value of the property. Any error produced by this method (or a TCL_BREAK or TCL_CONTINUE result) will result in configure producing an error; for errors, the message will be identical.

When configure determines that it wishes to write a property, -prop, it delegates the actual writing of the property to a method, <WriteProp-prop> (e.g., for a property -foo, the method will be <WriteProp-foo>) which is assumed to take one extra argument that is the value to write. Any error produced by this method (or a TCL_BREAK or TCL_CONTINUE result) will result in configure producing an error; for errors, the message will be identical. Successful results will be ignored.

Note that the property reader and writer methods described above are ordinary methods subject to normal override rules, and will likely be non-exported methods by default.

Property Declarations for Classes

When a class is a subclass of oo::configurable (or has it mixed in), it will have an extra declaration:

  • property name ?-option value...? ?name ?-option value...??...

This will declare one or more properties on the class under definition that can be configured by the instances of that class. The property will be named with a single minus sign preceding it (e.g., property foo creates something that can be configured as -foo). It will be an error for name to begin with a - character (because this is used to indicate options to the command that modify the nature of the property being defined), to not be a simple word (because it makes handling substitutions complex), or to not be suitable as a simple variable name (i.e., must not contain ::, ( or )).

This definition command is namespace exported from the namespace ::oo::configuresupport::configurableclass so that it may be used in other definition language namespaces easily.

(The command properties in the same namespace will also exist; it is just a pluralised alias. It will not be a documented feature.)

Property Declarations for Instances

When an object is an instance (direct or indirect) of oo::configurable (or has it mixed in), it will have an extra declaration:

  • property name ?-option value...? ?name ?-option value...??...

This is much the same as when classes are configurable, except that it applies the property definition changes to the instance.

This definition command is namespace exported from the namespace ::oo::configuresupport::configurableobject so that it may be used in other definition language namespaces easily.

(The command properties in the same namespace will also exist; it is just a pluralised alias. It will not be a documented feature.)

Property Definition Options

The -option arguments (and their values) of property definitions modify how the property works. The options for a property follow the name of that property; they are not shared between different properties declared at the same time.

In particular, these options are supported (all of which currently take a single value):

  • -kind propType

    This makes the property be readable, writable or read-write, depending on whether propType is readable, writable or readwrite respectively. If not specified, the property is read-write.

  • -get methodBody

    This allows you to specify the body of the implementation method for reading the property (whose name is derived from the property name as specified above) from the instances of the class or the object. If this option is not supplied and the property is of readable or read-write kind, a default will be used which just returns the contents of an instance variable with the same name as the property (without leading dash). No arguments will be supplied to the method.

    The default method body is (with <propNameWithoutDash> being substituted appropriately):

    ::set [my varname <propNameWithoutDash>]
    

    That is, for the declaration:

    property xyz
    

    The following getter method declaration will be done:

    method <ReadProp-xyz> -unexport {} {::set [my varname xyz]}
    
  • -set methodBody

    This allows you to specify the body of the implementation method for writing the property (whose name is derived from the property name as specified above) in instances of the class or the object. The method will take a single formal argument, value. If this option is not supplied and the property is of writable or read-write kind, a default will be used that just stores the argument in the instance variable with the same name as the property (without leading dash).

    The default method body is (with <propNameWithoutDash> being substituted appropriately):

    ::set [my varname <propNameWithoutDash>] $value
    

    That is, for the declaration:

    property xyz
    

    The following setter method declaration will be done:

    method <WriteProp-xyz> -unexport {value} {::set [my varname xyz] $value}
    

One of the main reasons for supplying a -set for a property is so that invalid values can be rejected; the default code does not reject any values at all. One of the main reasons for supplying a -get for a property is to allow the value returned to be computed rather than just simply held in a variable.

Note that if a class defines writable property with a particular name and another class defines a readable property with the same name, these can be brought together in the inheritance graph to form an apparent read-write property. This is by design.

Notes

Property Initialisation

This TIP provides no special mechanism for initialising a property. See the examples below for how to implement a simple mechanism using the configure method.

Interaction with Private Variables and Methods

This TIP has no special relationship with variable declarations or private declarations. In particular, it does nothing to alter those declarations, and it creates simple method declarations that need no particular declaration for the property storage variable. This means that other methods of the class may want to use variable to make the storage variable available, and may use private variable to create variables that are lightly shrouded from other classes.

However, a property declaration will not be private even if it is prefixed with a private command. This is because the implementation methods that it creates need to be accessed from outside the immediate class (i.e., from the configure method implementation). Instead, property implementation methods created by the property declaration are always unexported.

Examples

In particular, this means that if one does:

oo::configurable create Point {
    property x y
    constructor args {
        my configure -x 0 -y 0 {*}$args
    }
    variable x y
    method print {} {
        puts "x=$x, y=$y"
    }
}

You can then do:

set pt [Point new -x 27]
$pt print;   # x=27, y=0
$pt configure -y 42
$pt print;   # x=27, y=42
puts "distance from origin: [expr {
    hypot([$pt configure -x], [$pt configure -y])
}]";         # distance from origin: 49.92995093127971
puts [$pt configure]
             # -x 27 -y 42

That can then be extended with more properties by a subclass:

oo::configurable create Point3D {
    superclass Point
    property z
    constructor args {
        next -z 0 {*}$args
    }
}

set pt2 [Point3D new -x 2 -y 3 -z 4]
puts [$pt2 configure]
              # -x 2 -y 3 -z 4

Implementation

See the tip-558 branch.

Copyright

This document is placed in public domain.

History