Star Methods

Design study for implementing methods which applies to instances of instances meta-classes. This study implements in addition to the regular "method" a new construct called "*method" which has the mentioned transitive property. The same behavior can be achieved in many ways. In this study, we define a special class (the method container class for *methods) which is kept in the precedence path of instances. This way, it can be defined freely with other extension mechanisms such as mixins, traits or filters.

nx::Class eval {
    #
    # Define a *method, which is a method that applies for instances of
    # the instances of a meta-class.
    # - *methods are only defineable on meta-classes
    # - *methods are applicable on the instances of the instances of the
    #   meta-class
    # - If one defines a *method "bar" on a meta-class "MClass", and a
    #   class "C" as an instance of "MClass", and "c1" is an instance of
    #   "C", then "bar" is applicable for "c1".

    #
    # The "*method" has the same signature as regular methods, and can
    # be used in combination with the modifiers
    # public/protected/private as usual.
    #
    :public method *method {name arguments:parameter,0..* -returns body -precondition -postcondition} {
        #
        # Allow the definition only on meta-classes
        #
        if {![nsf::is metaclass [self]]} {
            error "[self] is not a meta-class"
        }
        #
        # Do we have the class for keeping the *methods already?
        #
        set starClass [nx::Class create [self]::*]

        if {![nsf::object::exists $starClass]} {
            #
            # If not, create the *method container class and provide
            # it as a default in the superclass hierarchy. This
            # happens by modifying the property "-superclasses" which
            # is used on every class to specify the class hierarchy.
            #
            :property [list superclasses $starClass] {
                #
                # Define a slot-specific method for keeping the
                # *method container class in the hierarchy.
                #
                :public object method appendToRelations { class property value } {
                    set sc [nsf::relation::get $class $property]
                    if {$sc eq "::nx::Object"} {
                        nsf::relation::set $class $property $value
                    } else {
                        nsf::relation::set $class $property [concat $sc $value]
                    }
                }

                #
                # Whenever the "-superclasses" relation is called,
                # make sure, we keep the *method container class in
                # the hierarchy.
                #
                :public object method value=set { class property value } {
                    :appendToRelations $class superclass $value
                }
            }

            #
            # Update class hierarchies of the previously created instances
            # of the meta-class.
            #
            foreach class [:info instances] {
                set slot [$class info lookup slots superclasses]
                $slot appendToRelations $class superclass $starClass
            }
        }

        #
        # Define the *method as regular method in the star method
        # container class.
        #
        [self]::* method $name $arguments \
            {*}[expr {[info exists returns] ? [list -returns $returns] : ""}] \
            $body \
            {*}[expr {[info exists precondition]  ? [list -precondition $precondition] : ""}] \
            {*}[expr {[info exists postcondition] ? [list -postcondition $postcondition] : ""}]
    }
}
set ::nsf::methodDefiningMethod(*method) 1

Some base test cases:

Define a meta-class MClass with a method "foo" and to star methods named "foo" and "bar".

nx::Class create MClass -superclass nx::Class {
    :public method foo {} {return MClass-[next]}
    :public *method foo {} {return *-[next]}
    :public *method bar {} {return *-[next]}
}

Define a class based on MClass and define here as well a method "foo" to show the next-path in combination with the *methods.

MClass create C {
    :public method foo {} {return C-[next]}
}

% C info superclasses
::MClass::*

Finally create an instance with the method foo as well.

C create c1 {
    :public object method foo {} {return c1-[next]}
}

The result of "foo" reflects the execution order: object before classes (including the *method container).

% c1 info precedence
::C ::MClass::* ::nx::Object
% c1 foo
c1-C-*-
% c1 bar
*-

Define a Class D as a specialization of C

MClass create D -superclass C {
    :public method foo {} {return D-[next]}
    :create d1
}

% d1 info precedence
::D ::C ::MClass::* ::nx::Object
% d1 foo
D-C-*-

Dynamically add *method "baz".

% d1 baz
::d1: unable to dispatch method 'baz'
MClass eval {
    :public *method baz {} {return baz*-[next]}
}
% d1 baz
baz*-

Test adding of *methods at a time, when the meta-class has already instances.

Create a meta-class without a *method

nx::Class create MClass2 -superclass nx::Class
MClass2 create X {:create x1}
% x1 info precedence
::X ::nx::Object

Now add a *method

MClass2 eval {
    :public *method baz {} {return baz*-[next]}
}

Adding the *method alters the superclass order of already created instances of the meta-class

% x1 info precedence
::X ::MClass2::* ::nx::Object
% x1 baz
baz*-

Finally, there is a simple application example for ActiveRecord pattern. All instances of the application classes (such as "Product") should have a method "save" (together with other methods now shown here). First define the ActiveRecord class (as a meta-class).

Class create ActiveRecord -superclass nx::Class {
    :property table_name

    :method init {} {
        if {![info exists :table_name]} {
            set :table_name [string tolower [namespace tail [self]]s]
        }
    }
    :public *method save {} {
        puts "save [self] into table [[:info class] cget -table_name]"
    }
}

Define the application class "Product" with an instance

ActiveRecord create Product
Product create p1
p1 save

The last command prints out: "save ::p1 into table products"