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:
Filter method 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?
You are correct (but I had to go and read the source carefully to confirm this). In a filter, filters are disabled, as without that you get all sorts of crazy things happening with reentrancy when you try to access all sorts of aspects of the current object.
The way to make what you're doing work is to use tailcall, specifically:
tailcall my {*}$argsToForward
# Or maybe, so you can only chain public methods:
# tailcall [self] {*}$argsToForward
With that one change, you get (cut-n-paste from an interactive session):
% person setName Bob setAge 100 details
Name set to Bob
Age set to 100
Bob is 100 years old
The tailcall means that this won't work too well with multiple filters... but the whole idea of stacking multiple filters (especially ones like yours!) just makes my brain ache.
Congratulations, by the way, on finding a new (to me) use for filters that I'd never considered before.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With