I am trying to hold a reference to a variable as a data member in tclOO. Is there any construct that can simulate the upvar command that is used inside functions.
oo::class create TestClass {
variable refToVar
constructor {varRef} {
set varRef [string range $varRef 1 [expr [string length $varRef] -1]]
# upvar $varRef temp
# set refToVar $temp
upvar $varRef refToVar
}
method getRefToVar {} {
return $refToVar
}
method setRefToVar {value} {
set refToVar $value
}
}
proc testProc {varRef} {
set varRef [string range $varRef 1 [expr [string length $varRef] -1]]
upvar $varRef temp
set temp 5
}
puts "start"
set x 100
puts $x
puts "proc test"
testProc {$x}
puts $x
puts "tclOO test"
TestClass create tst {$x}
puts [tst getRefToVar]
tst setRefToVar 20
puts [tst getRefToVar]
puts $x
Basically i want to achieve the same behaviour with the oo::class that I am doing with the proc.
The equivalent is upvar. However you need to run the upvar in the right context, that of the object's state namespace and not the method (because you want refToVar to be by the overall object) because upvar always makes its references from in the current stack context, which means local variables in both cases by default.
TclOO provides the tools though.
my eval [list upvar 2 $varref refToVar]
It's a 2 because there's one level for my eval and another level for the method. MAKE SURE that you only refer to variables in a namespace (global variables are in the global namespace) this way or you'll have pain. It's probably not a good idea to keep references to variables around inside an object as they can be run from all sorts of contexts, but it should work.
You can also use the namespace upvar command inside that generated my eval to do the same sorts of thing, since anything you can write that is sensible can be described using namespace/varname coordinates instead of stack-index/varname coordinates.
[EDIT]: In fact, Tcl takes care to avoid the evil case, and throws an error if you try. Here's a boiled-down version of the class:
oo::class create Foo {
constructor v {
# This is done like this because this sort of operation is not affected
# by the 'variable' declaration; it's a bit unusual...
my eval [list upvar 2 $v ref]
}
variable ref
method bar {} {
# Just some normal usage
puts $ref
}
}
Let's show this working normally:
% set x 123
123
% set y [Foo new x]
::oo::Obj12
% $y bar
123
% incr x
124
% $y bar
124
OK, that all looks good. Now, let's go for attaching it to a local variable:
proc abc {} {
set x 234
set y [Foo new x]
# Try the call
$y bar
incr x
$y bar
# Try returning it to see what happens with the lifespan
return $y
}
But calling it doesn't work out:
% abc
bad variable name "ref": can't create namespace variable that refers to procedure variable
% puts $errorInfo
bad variable name "ref": can't create namespace variable that refers to procedure variable
while executing
"upvar 2 x ref"
(in "my eval" script line 1)
invoked from within
"my eval [list upvar 2 $v ref]"
(class "::Foo" constructor line 1)
invoked from within
"Foo new x"
(procedure "abc" line 3)
invoked from within
"abc"
So there you go; you can link an external global (or namespace) variable easily enough into an object, but it had better not be a local variable. It might be possible to do extra work to make using a local variable that is actually an upvar to a global variable work, but why would you bother?
Donal has answered your question, but there is a better way to do it.
oo::class create TestClass {
variable varRef
constructor args {
lassign $args varRef
}
method getRefToVar {} {
upvar 0 $varRef refToVar
return $refToVar
}
method setRefToVar {value} {
upvar 0 $varRef refToVar
set refToVar $value
}
}
Here, you supply the TestClass object with the qualified name of the variable, like ::x for the global variable x, or ::foobar::x for the x variable in the foobar namespace. You save this name in the instance variable varRef.
When you want to read from or write to this variable, you first create a local reference to it:
upvar 0 $varRef refToVar
This means that in the method where you are invoking this command, refToVar is a reference or alias to the variable whose name is stored in varRef.
You can do the same thing in a command procedure:
proc testProc {varRef} {
upvar 0 $varRef refToVar
set refToVar 5
}
Testing it:
puts "start"
set x 100
puts $x
puts "proc test"
testProc ::x
puts $x
puts "tclOO test"
TestClass create tst ::x
puts [tst getRefToVar]
tst setRefToVar 20
puts [tst getRefToVar]
puts $x
Note how the names are written differently.
If you somehow don't know the qualified name of the variable, this should be helpful:
TestClass create tst [namespace which -variable x]
BTW: you don't have to write
string range $varRef 1 [expr [string length $varRef] -1]
there is an abbreviated form for that:
string range $varRef 1 end
but you don't have to write that either: to remove zero or more dollar signs from the left end of a string, just
string trimleft $varRef \$
but you don't have to write that either: you don't need to add the dollar sign when passing a variable name:
proc bump varName {
upvar 1 $varName var
incr var
}
set x 10
# => 10
bump x
set x
# => 11
Documentation: expr, incr, lassign, namespace, proc, puts, set, string, upvar
Related
i want to execute this code but it is not waorking , the tcl script is as follows:
set i 0
foreach pattern { tiger cat horse dog} {
set pat$i abc
puts "pat$i=${pat$i}"
set i [expr {$i + 1}]
}
the desired result i want in every loop is :
pat0=abc
pat1=abc
pat2=abc
pat3=abc
Please help me to find my mistake
You can read a variable with a computed name with the one-argument form of set:
set i 0
foreach pattern { tiger cat horse dog} {
set pat$i abc
puts "pat$i=[set pat$i]"
set i [expr {$i + 1}]
}
The $... syntax can be considered shorthand for that (except with different, more limited parsing rules that make it more convenient to write).
Alternatively, you can use upvar 0 to make a variable alias with a simple name:
set i 0
foreach pattern { tiger cat horse dog} {
upvar 0 pat$i p
set p abc
puts "pat$i=$p"
set i [expr {$i + 1}]
}
That is much more favoured in a procedure.
HOWEVER, are you sure you need the variable to be called that? Can you use an associative array instead? That's often just as convenient.
set i 0
foreach pattern { tiger cat horse dog} {
set pat($i) abc
puts "pat$i=$pat($i)"
incr i; # The incr command is *right there*
}
I'm trying to wrap my head around Tcl variable scopes, but I'm stuck on what I thought would be a simple concept: how can I access a variable that I've defined outside of my proc, but that I do not explicitly pass to the proc?
I'm trying to avoid setting a bunch of global variables, and only access variables that I define within a particular namespace. What do I need to add to my code below so that the proc can access the variable a, which is obviously not in the scope of the proc?
set a apples
proc myList {b c} {
puts [concat $a $b $c]
}
You can use upvar:
set a apples
proc myList {b c} {
upvar a a
puts [concat $a $b $c]
}
Or, expanding the example a bit to show the "source" variable does not have to exist in the global scope:
proc p1 {} { set a 10; p2 }
proc p2 {} { upvar 1 a b; puts "in p2, value is $b" }
p1
outputs
in p2, value is 10
If a was defined in a namespace, you can use variable:
namespace eval foo {
set a apples
# OR
# variable a apples
}
proc foo::myList {b c} {
variable a
puts [concat $a $b $c]
}
Or if a was created in the global scope, you can still access it without the global function, using :: (I'll reference this SO question for this one):
proc myList {b c} {
puts [concat $::a $b $c]
}
If I define the following list:
set ::listofvariablesn [list {"inputfile" "Input file"}]
set ::inputfile "Somefile or numbers"
I can check if the variable ::inputfile exists using:
foreach a $::listofvariablesn {
if {[info exists ::[lindex $a 0]]} {
puts "Ok"
}
}
But I cannot access the value of ::inputfile using:
foreach a $::listofvariablesn {
if {[checkvl "Input file" $::[lindex $a 0] 1 0 0 0]} {
puts "Ok"
}
}
I got the error:
can't read "::": no such variable
Is there a way to get this substitution done? I mean turn $::[lindex $a 0] into $::inputfile so I can access the value of ::inputfile
Thanks a lot
The $ is a shortcut, and can't do double substitution.
Use the set command:
if {[checkvl "Input file" [set ::[lindex $a 0]] 1 0 0 0]} {
Apart from the longhand form of $, the one-argument form of the set command, there is also the option to create an alias of the variable (with a known name) using upvar 0. For example:
upvar 0 $a b
puts "exists: [info exists b]"
puts "value: $b"
Anything you do to the variable b (except changing the upvar) will be performed on the variable that was named by $a.
I have a set of procs and a namespace as you can see below:
namespace eval ::_API {
if {[info exists ::_API::API_ids]} {
catch {API erase -ids [array names ::_API::API_ids]}
}
catch {unset API_ids}
array set API_ids ""
}
proc ::_API::erase { } {
foreach id [array names ::_API::API_ids] {
if {::_API::API_ids($id) == 0} {
continue
}
if {[catch {API -id $id -redraw 0}] != 0} {
set ::_API::API_ids($id) 0
}
}
Redraw ;# I'm not concerned about this part
# and I'm fairly certain it can be ignored
}
proc erase { } {
::_API ::erase
}
::_API::API_ids is an array that contains points (e.g. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15). What the script does is erase points in a table.
I want to convert the namespace ::_API into a proc so that I can use a GUI button to call the proc. It is currently directly after some other scripts (which map the points in the table) and I want to erase them only when required; i.e. when the button will be pressed.
I already tried running ::_API::erase directly but it is not working:
proc ::_API::erase { } {
foreach id [array names ::_API::API_ids] {
if {::_API::API_ids($id) == 0} {
continue
}
if {[catch {API -id $id -redraw 0}] != 0} {
set ::_API::API_ids($id) 0
}
}
Redraw
}
I think that there might be something I'm missing about the namespace. I tried reading the documentation but I don't quite understand really how they work.
The first thing that you really must do is use variable to declare the variable. For some fairly ugly reasons, failing to do that can cause “fun” with variable resolution to make things happen in ways you don't expect:
namespace eval ::_API {
variable API_ids; ##### <<<<<<< THIS <<<<<<< #####
if {[info exists ::_API::API_ids]} {
catch {API erase -ids [array names ::_API::API_ids]}
}
catch {unset API_ids}
array set API_ids ""
}
Secondly, you probably ought to actually think in terms of using real OO for this rather than trying to fake it. For example, with TclOO you'd be writing something like:
oo::class create APIClass {
variable ids
constructor {} {
array set ids {}
}
method erase {} {
foreach id [array names ids] {
if {$ids($id) == 0} continue
if {[catch {
API -id $id -redraw 0
}]} {
set ids($id) 0
}
}
Redraw
}
# Allow something to reference the ids variable from the outside world
method reference {} {
return [my varname ids]
}
}
APIClass create _API
# [_API erase] will call the erase method on the _API object
This simplifies things quite a bit, and in fact you can think in terms of coupling the drawing and the data management quite a lot closer than I've done above; it's just indicative of what you can do. (I find that it makes stuff a lot simpler when I use objects, as they've got a much stronger sense of lifecycle about them than ordinary namespaces.)
What you mean is you want to convert the namespace initialization code into a procedure. The following example should achieve that.
namespace eval ::_API {
}
proc ::_API::initialize {} {
variable API_ids
if {[info exists API_ids]} {
catch {API erase -ids [array names API_ids]}
unset API_ids
}
array set API_ids ""
}
... more definitions ...
::_API::initialize
We start by declaring the namespace. Then replicate the original code in a procedure. As there is no point unsetting a non-existent variable, we move unset into the block that only runs if the variable exists.
At the end of the namespace definitions, initialize the namespace by calling its initialization function.
I'm creating a dynamic variable ("Variable variable" in PHP parlance) with the following:
foo: "test1"
set to-word (rejoin [foo "_result_data"]) array 5
But how do I get the value of the resulting variable named "test1_result_data" dynamically? I tried the following:
probe to-word (rejoin [foo "_result_data"])
but it simply returns "test1_result_data".
As your example code is REBOL 2, you can use GET to obtain the value of the word:
>> get to-word (rejoin [foo "_result_data"])
== [none none none none none]
REBOL 3 handles contexts differently from REBOL 2. So when creating a new word you will need to handle it's context explicitly otherwise it will not have a context and you'll get an error when you try to set it. This is in contrast to REBOL 2 which set the word's context by default.
So you could consider using REBOL 3 code like the following to SET/GET your dynamic variables:
; An object, providing the context for the new variables.
obj: object []
; Name the new variable.
foo: "test1"
var: to-word (rejoin [foo "_result_data"])
; Add a new word to the object, with the same name as the variable.
append obj :var
; Get the word from the object (it is bound to it's context)
bound-var: in obj :var
; You can now set it
set :bound-var now
; And get it.
print ["Value of " :var " is " mold get :bound-var]
; And get a list of your dynamic variables.
print ["My variables:" mold words-of obj]
; Show the object.
?? obj
Running this as a script yields:
Value of test1_result_data is 23-Aug-2013/16:34:43+10:00
My variables: [test1_result_data]
obj: make object! [
test1_result_data: 23-Aug-2013/16:34:43+10:00
]
An alternative to using IN above could have been to use BIND:
bound-var: bind :var obj
In Rebol 3 binding is different than Rebol 2 and there are some different options:
The clumsiest option is using load:
foo: "test1"
set load (rejoin [foo "_result_data"]) array 5
do (rejoin [foo "_result_data"])
There is a function that load uses--intern--which can be used to bind and retrieve the word to and from a consistent context:
foo: "test1"
set intern to word! (rejoin [foo "_result_data"]) array 5
get intern to word! (rejoin [foo "_result_data"])
Otherwise to word! creates an unbound word that is not easy to utilize.
The third option is to use bind/new to bind the word to a context
foo: "test1"
m: bind/new to word! (rejoin [foo "_result_data"]) system/contexts/user
set m array 5
get m
probe do (rejoin [foo "_result_data"])
from http://www.rebol.com/docs/core23/rebolcore-4.html#section-4.6