Working with TCL and I'd like to implement something like the Strategy Pattern. I want to pass in the "strategy" for printing output in a TCL function, so I can easily switch between printing to the screen and printing to a log file. What's the best way to do this in TCL?
TCL allows you to store the name of a procedure in a variable and then call the procedure using that variable; so
proc A { x } {
puts $x
}
set strat A
$strat Hello
will call the proc A and print out Hello
In addition to the answer showing how you assign a procedure to a variable, you can also pass the name of a procedure as an argument to another procedure. Here's a simple example:
proc foo { a } {
puts "a = $a"
}
proc bar { b } {
puts "b = $b"
}
proc foobar { c } {
$c 1
}
foobar foo
foobar bar
This will print a = 1 and b = 1
A slightly expanded example of what was listed above that might illustrate the Strategy Pattern more clearly:
proc PrintToPDF {document} {
<snip logic>
}
proc PrintToScreen {document} {
<snip logic>
}
proc PrintToPrinter {document} {
<snip logic>
}
set document "my cool formatted document here"
set printMethod "printer"
switch -- $printMethod {
"printer" {
set pMethodName "PrintToPrinter"
}
"pdf" {
set pMethodName "PrintToScreen"
}
"screen" {
set pMethodName "PrintToPDF"
}
}
$pMethodName $document
Aside from using a proc, you could actually use a code block instead. There are a few variations on this. first is the most obvious, just eval
ing it.
set strategy {
puts $x
}
set x "Hello"
eval $strategy
unset x
This works, but there are a few downsides. First the obvious, both pieces of code must collude to using a common naming for the arguments. This replaces one namespace headache (procs) with another (locals), and this is arguably actually worse.
Less obvious is that eval deliberately interprets its argument without compiling bytecode. This is because it is assumed that eval will be called with dynamically generated, usually unique arguments, and compiling to bytecode would be inefficient if the bytecode would only be used once, relative to just interpreting the block immediately. This is easier to fix, so here's the idiom:
set x "Hello"
if 1 $strategy
unset x
if
, unlike eval
, does compile and cache its code block. If the $strategy
block is only ever one or just a handful of different possible values, then this works very well.
This doesn't help at all with the yuckiness of passing arguments to the block with local variables. There are a lot of ways around that, such as doing substitutions in the same way tk does substitutions on command arguments with %
's. You can try doing some hackish things using up uplevel
or upvar
. For example you could do this:
set strategy {
puts %x
}
if 1 [string map [list %% % %x Hello] $strategy]
On the off chance that the arguments being passed don't change very much, this works well in terms of bytecode compilation. If on the other hand, the argument changes often, you should use eval
instead of if 1
. This isn't much better anyway, in terms of arguments. There's less likelyhood of confusion about what's passed and what's not, because you're using a special syntax. Also this is helpful in case you want to use variable substitution before returning a code block: as in set strategy "$localvar %x"
.
Fortunately, tcl 8.5 has true anonymous functions, using the apply
command. The first word to the apply command would be a list of the arguments and body, as if those arguments to proc
had been lifted out. The remaining arguments are passed to the anonymous command as arguments immediately.
set strategy [list {x} {
puts $x
}]
apply $strategy "Hello"
% set val 4444
4444
% set pointer val
val
% eval puts $$pointer
4444
% puts [ set $pointer ]
4444
% set tmp [ set $pointer ]
4444
How about using variable functions? I don't remember much TCL (it's been a while...) but maybe one of these would do what you need:
- [$var param1 param2]
- [$var] param1 param2
- $var param1 param2
If i'm wrong, anyone is free to correct me.
To clarify why Jackson's method works, remember that in TCL, everything is a string. Whether you are working with a literal string, a function, a variable, or whatever it may be, everything is a string. You can pass a "function pointer" just like you can a "data pointer": simply use the object's name with no leading "$".
All what stated above, although when moving from namespace to namespace, you may want to use as a passing [namespace current ]::proc_name
, for ensuring that you don't get any breaks.
For OO methods, you'll need to follow what is in this thread:Pass a method of a specific object as an input argument in Tcl
Godspeed.
© 2022 - 2024 — McMap. All rights reserved.