Tcl's object-oriented system allows for the use of Filter methods that can be used to intercept method invocations on a class's instances.
This functionality can be used to replicate Smalltalk's style of method chaining, which means these three individual statements
person setName Bob
person setAge 100
person details
Could instead be chained together and written as
person setName Bob setAge 100 details
The code below implements this functionality by defining a Filter method to do the following:
- Given the requested method name look up its implementation and the number of parameters it requires.
- Split the parameters captured by the filter between the target method -- sending it only what it requires (determined in step 1) -- and then forwarding the remaining parameters to the instance again to trigger the method dispatch /
Filtermethod functionality.
In essence, this logic is de-sugaring the single-statement method chain example above into the three statement version.
oo::class create Person {
variable Name Age
method MethodChaining {args} {
# Get the target method's name and its associated class.
lassign [self target] className methodName
# puts "$className :: $methodName"
# Retrieve the target method's definition,
# specifically its call signature / what parameters it takes.
lassign [info class definition $className $methodName] methodArgs
if {[llength $args] == 0} {
# If the filtering method was invoked with no arguments,
# there is nothing to forward to the target method.
return [next]
} elseif {$methodArgs eq "args"} {
# If the target method has specified the "args" parameter
# name, we forward all of the filtering methods arguments.
#puts "Target method takes unlimited args."
return [next {*}$args]
} else {
# Split the filter's arguments up to satisfy
# the target method's required number of arguments
# and then forward the rest to the object instance
# to trigger method dispatch.
set numMethodArgs [llength $methodArgs]
set targetArgs [lrange $args 0 $numMethodArgs-1]
set argsToForward [lrange $args $numMethodArgs end]
# puts "targetArgs: $targetArgs"
# puts "argsToForward: $argsToForward"
next $targetArgs
if {[llength $argsToForward] != 0} {
my {*}$argsToForward
}
}
}
method multi {args} {
puts "Hello $args"
}
method setName {name} {
puts "Name set to $name"
set Name $name
}
method setAge {age} {
puts "Age set to $age"
set Age $age
}
method details {} {
puts "$Name is $Age years old"
}
filter MethodChaining
}
My implementation appears to be working, but with some surprising results.
When the below is invoked
person setName Bob setAge 100 details
The following is output:
Name set to Bob
wrong # args: should be "my setAge age"
The fact that we see "Name set to Bob" means that the filter implementation is working correctly and that the messages are being split up as expected: setName is being called with a single argument of "Bob". However, the warning about wrong # of args sent to setAge is unexpected -- as we've just seen, the filter implementation has no problem dealing with method calls that are receiving more than the specified number of arguments. The fact that this message appears indicates that the internal call of my setAge 100 details is not invoking the Filter itself. If it were, setAge should not complain about the additional parameter.
The main difference between the initial call of
person setName Bob setAge 100 details
and the internal call of
my setAge 100 details
is that the first originates from outside the Filter while the second originates from inside the Filter.
I had suspected that perhaps it was the use of my, but using [self object] in its place failed in the same manner.
Likewise, I also attempted to invoke subsequent "forwarded" messages via uplevel, but again, got the same error about wrong number of arguments.
To finally get to my question: do methods invoked from within Filter methods follow the same dispatch procedures as normal methods, or do they bypass the Filtering phase?