Suppose the following code declarations:
itcl::class ObjectA {
private variable m_ownedObject
private variable m_someVariable
constructor {} \
{
set m_ownedObject [ObjectA #auto]
}
protected method SetSomeVariable {newVal} {
set m_someVariable $newVal
}
public method SomeMethod{} {
$m_ownedObject SetSomeVariable 5
}
}
This is the only way I know how to modify m_someVariable from within SomeMethod on m_ownedObject. In other languages (say C/C++/C#/Java to name a few), I'm pretty sure I could just say something like:
m_ownedObject.m_someVariable = 5
Is there a way to do something like this in tcl, or do I always need to create protected getters and setters? Hopefully this is reasonably clear.
You cannot directly do what you're asking for in itcl. However, this being Tcl, you can work around that, and directly set the member variable from anywhere. I use a helper routine called memv which you pass an instance and a variable name, and it returns a "reference" to that variable.
This obviously bypasses the private/protected mechanisms that Itcl set up, so you're violating abstractions using them. It's your call whether you want to use it. I find it invaluable for debugging, but don't it in production code.
The example usage is:
set [memv m_ownedObject m_someVariable] 5
The code for memv is:
proc memv {obj varname} {
# have to look up the variable, which might be in a base class
# so do 'info variable' to get that, and the full name is the 3rd element
# next two lines handle pulling apart an array
set aindex ""
regexp -- {^(.+)\((.+)\)$} $varname ignore varname aindex
set var [lindex [$obj info variable $varname] 2]
if {$aindex == ""} {
return [list #itcl $obj $var]
} else {
return [list #itcl $obj $var\($aindex\)]
}
}
Similarly, I have a helper routine named memv which allows you to call any method (including private and protected methods). It's usage is similar
[memf m_ownedObject SetSomeVariable] 5
And it's code is:
proc memf {obj fcnname} {
set f [$obj info function $fcnname]
if {[llength $f] != 5} {
error "expected '$obj info function $fcnname' to return something like 'private proc ::namespace::name args {...}' but got: $f"
}
set fullname [lindex [$obj info function $fcnname] 2]
set namespace [namespace qualifiers $fullname]
set function [namespace tail $fullname]
return [itcl::code -namespace $namespace $obj $function]
}
$m_ownedObject configure -m_someVariable 5
If you're declaring a variable as private, means that can be only accessed from within the class. And that's also valid for C/C++/Java ... so I'm not sure what are you expecting.
Anyway Tcl is a dynamic language, so you can do something like that.
itcl::class tclass {
foreach v {time distance} {
method get$v {} [subst -nocommands { return [subst $$v] }]
method set$v nuval [subst -nocommands { set $v \$nuval } ]
protected variable $v "Var $v"
}
}
And it will create all the getters and setters that you need ;)
You can find more info here: http://wiki.tcl.tk/17667
Related
How can one execute an object method as a variable?
oo::class create handlerTest {
method onEvent {} {
puts "onEvent method"
}
}
set testObj [handlerTest new]
#set wrapper {$testObj onEvent}
#set wrapper {$testObj::my onEvent}
#set wrapper [namespace code {$testObj onEvent}]
#set wrapper "eval testObj onEvent
#set wrapper {[eval testObj onEvent]}
$wrapper
All of the above attempts appear to execute $wrapper as a single command, not a command with args.
As I am using an external library that calls the defined wrapper, I can't change how the wrapper is called (i.e. {*}$wrapper).
Is there a way to do this?
Or:
proc theWrapper {} [
upvar 1 testObj testObj
tailcall $testObj onEvent
}
set wrapper theWrapper
$wrapper
The only way to rewrite the command name itself is via an unknown handler (defaults to being called unknown in the global namespace; you probably want to use that default). Some care needs to be taken when doing this, as the default handler does things that some code needs to have present; a bit of shuffling around with rename should do the trick.
# only want special treatment for some commands
set autoexpanded [list $testObj]
# save for later
rename unknown _original_unknown
proc unknown args {
global autoexpanded
# if we want to expand the first word...
if {[catch {lindex $args 0 0} cmd] == 0 && $cmd in $autoexpanded} {
# delegate to the expanded command (tailcall is perfect here)
set args [lassign $args cmd]
tailcall {*}$cmd {*}$args
} else {
# delegate to the original unknown
tailcall _original_unknown {*}$args
}
}
Be aware that this is not a fast dispatch mechanism. It is only used when the only other alternative is throwing an error because the command doesn't exist (also slow, but error paths are never optimal or heavily optimized).
The easiest method that comes to mind, is to generate a name for an alias and put that in the variable:
set testObj [handlerTest new]
set wrapper [interp alias {} wrapper[incr wrapperid] {} $testObj onEvent]
$wrapper
=> onEvent method
I am new to the TCL scripting .I have a script called "Sample.tcl". In the Sample.tcl I have a variable called $name. How can I assign a value to the variable if there exist a specific argv i.e.
Sample.tcl -step xyz
Only if I specify -step then $name should be xyz.
I'm not sure what $name might be in this context (it's a really unusual name for a variable, and using variable variable names is typically a bad idea) but under the guess that you're trying to set step to xyz in this case, you can put this in your script:
apply {{} {
# For each pair of values in the arguments (after the script name)
global argv
foreach {key value} $argv {
# Safety-check: if the key starts with a hyphen...
if {[string match -* $key]} {
# ... strip the leading hyphen(s)
set varname [string trimleft $key "-"]
# ... bind that global var name to a local name
upvar 1 $varname var
# ... and set the variable to the value we've got.
set var $value
}
}
}}
It's done in an apply so that we don't pollute the global namespace with all our working variables (key, value, varname and var) and because we don't really need to make a procedure for something we're only going to do once.
This isn't a safe piece of code, not by any means, but it is a useful and flexible way to get something working.
In general, parsing command line arguments can take quite a bit of thought to get perfectly right and there's various packages to help out, but that's only really important when writing code for other people to run. When it's just for yourself, you can be a lot sloppier and get the job done in a few minutes.
Using the cmdline package from tcllib you could write:
#!/usr/bin/env tclsh
package require cmdline
set options {
{step.arg "" "Set the step value"}
}
try {
array set params [cmdline::getoptions argv $options]
} on error e {
puts stderr $e
exit 1
}
if {$params(step) ne ""} {
set name $params(step)
}
if {[info exists name]} {
puts "name = $name"
} else {
puts "name is not set"
}
I wrote the following proc, which simulates the filter function in Lodash (javascript library) (https://lodash.com/docs/4.17.4#filter). You can call it in 3.5 basic formats, seen in the examples section. For the latter three calling options I would like to get rid of the the requirement to send in -s (shorthand). In order to do that I need to differentiate between an anonymous proc and a list/dict/string.
I tried looking at string is, but there isn't a string is proc. In researching here: http://wiki.tcl.tk/10166 I found they recommend info complete, however in most cases the parameters would pass that test regardless of the type of parameter.
Does anyone know of a way to reliable test this? I know I could leave it or change the proc definition, but I'm trying to stay as true as possible to Lodash.
Examples:
set users [list \
[dict create user barney age 36 active true] \
[dict create user fred age 40 active false] \
]
1. set result [_filter [list 1 2 3 4] {x {return true}}]
2. set result [_filter $users -s [dict create age 36 active true]]
3. set result [_filter $users -s [list age 36]]
4. set result [_filter $users -s "active"]
Proc Code:
proc _filter {collection predicate args} {
# They want to use shorthand syntax
if {$predicate=="-s"} {
# They passed a list/dict
if {[_dictIs {*}$args]} {
set predicate {x {
upvar args args
set truthy 1
dict for {k v} {*}$args {
if {[dict get $x $k]!=$v} {
set truthy false
break
}
}
return $truthy
}}
# They passed just an individual string
} else {
set predicate {x {
upvar args args;
if {[dict get $x $args]} {
return true;
}
return false;
}}
}
}
# Start the result list and the index (which may not be used)
set result {}
set i -1
# For each item in collection apply the iteratee.
# Dynamically pass the correct parameters.
set paramLen [llength [lindex $predicate 0]]
foreach item $collection {
set param [list $item]
if {$paramLen>=2} {lappend param [incr i];}
if {$paramLen>=3} {lappend param $collection;}
if {[apply $predicate {*}$param]} {
lappend result $item
}
}
return $result
}
Is x {return true} a string, a list, a dictionary or a lambda term (the correct name for an anonymous proc)?
The truth is that it may be all of them; it would be correct to say it was a value that was a member of any of the mentioned types. You need to describe your intent more precisely and explicitly rather than hiding it inside some sort of type magic. That greater precision may be achieved by using an option like -s or by different main command names, but it is still necessary either way. You cannot correctly and safely do what you seek to do.
In a little more depth…
All Tcl values are valid as strings.
Lists have a defined syntax and are properly subtypes of strings. (They're implemented differently internally, but you are supposed to ignore such details.)
Dictionaries have a syntax that is equivalent to lists with even numbers of elements where the elements at the even indices are all unique from each other.
Lambda terms are lists with two or three elements (the third element is the name of the context namespace, and defaults to the global namespace if it is absent). The first element of the list needs to be a valid list as well.
A two-element list matches the requirements for all the above. In Tcl's actual type logic, it is simultaneously all of the above. A particular instantiation of the value might have a particular implementation representation under the covers, but that is a transient thing that does not reflect the true type of the value.
Tcl's type system is different to that of many other languages.
I'm trying to write a TclOO wrapper around the http package. The idea is to create an object passing the arguments that you would normally pass to http::geturl and have code, data, ncode and meta methods to return the same data as the corresponding http:: proc. To avoid lots of typing, I hit upon the following:
oo::class create myHttp {
constructor {url args} {
set responses {data code ncode meta}
foreach response $responses {
oo::objdefine [self object] variable -append m_$response
oo::objdefine [self object] method $response {} [subst -nocommands {
puts stdout [lsort [info vars *]]
return [set m_$response]
}]
}
set tok [http::geturl $url {*}$args]
foreach response $responses {
set m_$response [http::$response $tok]
}
http::cleanup $tok
}
Sadly it doesn't work.
% set h [myHttp new http://www.google.co.uk]
% $h code
m_code m_data m_meta m_ncode
can't read "m_code": no such variable
What's going on here? The output of [info vars] suggests that m_code exists, but the error says it doesn't. Accessing the variables directly from the [info object namespace $h] namespace shows that they all exist and have the expected values but all the accessor methods fail with the same error.
Is there a better way to "auto-generate" the methods and variables? I freely admit that I haven't quite got my head around defining methods using oo::objdefine rather than in an oo::class create statement, and I've tried adding, semi-randomly, my variable m_$response and variable m_$response statements in the $reponse methods to no avail. I'm concerned to find myself using subst here as I've usually found on later review that a simple list would have worked adequately and less opaquely.
For completeness, I'm using Tcl 8.6.4 using an ActiveState distribution on Windows 7, and debugging with TkCon.
I'd write it as
oo::class create myHttp {
variable tok
constructor {url args} {
set tok [http::geturl $url {*}$args]
foreach response {data code ncode meta} {
oo::objdefine [self] forward $response http::$response $tok
}
}
destructor {
http::cleanup $tok
}
}
but if you really need the variables for some purpose, that obviously won't work.
I just started to learn Tcl. I wanted to write a simple procedure.
When the procedure starts, it opens a browse window to browse for files.
There you can select a file you want to open.
Then a pop-up windows comes up and asks if you want to selected another file.
Every file that you select has to go into an array.
I have to following code:
########## Defining the sub procedures ############
proc open_file {} {
set n 0
set title "Select a file"
set types {
{{GDS files} {.gds} }
{{All Files} * }
}
set filename [tk_getOpenFile -filetypes $types -title $title]
set opendFiles($n) $filename
set n [expr $n + 1]
set answer [tk_messageBox -message "Load another GDS file?" -type yesno -icon question]
if {$answer == yes } {
open_file
} else {
show_files ($opendFiles)
}
}
proc show_files {} {
foreach key [array names opendFiles] {
puts $opendFiles($key)
}
}
########## Main Program ###########
open_file
I having the following problems. Because I always recall the proc 'open_file' the variable $n keeps setting to 0. But I don't know how to recall the opening of the window without recalling the whole subroutine....
The second problem is sending the array to the next proc. When I send to the to the proc 'show_files', I always get the next error : can't read "opendFiles": variable is array.
I can't seem to find both answers..
You need global variables for that. This works for me:
########## Defining the sub procedures ############
set n 0
array set openedFiles {}
proc open_file {} {
set title "Select a file"
set types {
{{GDS files} {.gds} }
{{All Files} * }
}
set filename [tk_getOpenFile -filetypes $types -title $title]
set ::openedFiles($::n) $filename
incr ::n
set answer [tk_messageBox -message "Load another GDS file?" -type yesno -icon question]
if {$answer == yes } {
open_file
} else {
show_files
}
}
proc show_files {} {
foreach key [array names ::openedFiles] {
puts $::openedFiles($key)
}
}
########## Main Program ###########
open_file
Array Problem
In Tcl you can't send arrays to procs. You need to convert them to a list with array get send this list to the proc and than convert it back to an array again with array set.
Global variables are very useful at times, but I believe they are best avoided where possible. In this case I'd rather process the loop and the array in the main program rather than the proc.
Also, where you'd use an array in other programming languages, it's often better to use a list in Tcl, so something like:
proc open_file {} {
set title "Select a file"
set types {
{{GDS files} {.gds} }
{{All Files} * }
}
set filename [tk_getOpenFile -filetypes $types -title $title]
return $filename
}
proc show_files {files} {
foreach file $files {
puts $file
}
}
set openedFiles [list]
set answer yes
while {$answer == yes}
lappend openedFiles [open_file]
set answer [tk_messageBox -message "Load another GDS file?" -type yesno -icon question]
}
show_files $openedFiles
If you're into brevity, show_files could be written
proc show_files {files} {
puts [join $files \n]
}
and, now that it's so short, you could just put it in line, rather than have another proc.
Finally, have you considered what you want to do if the user presses cancel in tk_getOpenFile? In this case filename will be set to an empty (zero-length) string. You could either
ignore these; or
get rid of the tk_messageBox call and have the user press cancel when they have entered as many files as they want.
If you want to just ignore those times when the user pressed cancel, you could do
set filename [open_file]
if {[string length $filename] > 0} {
# The user entered a new filesname - add it to the list
lappend openedFiles $filesname
} else {
# The user pressed cancel - just ignore the filename
}
If you wanted to use cancel to break out of the loop, then the main program becomes something like:
set openedFiles [list]
set filename dummy
while {[string length $filename] > 0} {
set filename [open_file]
if {[string length $filename] > 0} {
lappend openedFiles $filename
}
}
show_files $openedFiles
in this case, you might want to put up a message box right at the start of the main program telling the user what's going on.
For the state of a variable to persist between calls to a procedure, you need to make that variable live outside the procedure. The easiest way is to use a global variable:
# Initialize it...
set n 0
proc open_file {} {
# Import it...
global n
...
# Use it...
set openedFiles($n) $filename
incr n
...
}
Arrays are not values, and as such can't be passed directly to another procedure. You can handle this by passing in the name and using upvar 1 to link a local alias to the variable in the calling stack frame:
proc show_files {varName} {
upvar 1 $varName ary
foreach key [array names ary] {
puts $ary($key)
}
}
Which is called using the name of the array, so no $:
show_files openedFiles
(You could also pass a serialization of the array in with array get openedFiles to serialize and array set ary $serialization to deserialize, but that carries some overhead.)
You probably ought to add that openedFiles variable to the global line, so that it is persistent across all invokations of open_file.