Determine type of a variable in Tcl
Asked Answered
I

6

13

I'm looking for a way to find the type of a variable in Tcl. For example if I have the variable $a and I want to know whether it is an integer.

I have been using the following so far:

    if {[string is boolean $a]} {
    #do something
    }

and this seems to work great for the following types:
alnum, alpha, ascii, boolean, control, digit, double, false, graph, integer, lower, print, punct, space, true, upper, wordchar, xdigit

However it is not capable to tell me if my variable might be an array, a list or a dictionary. Does anyone know of a way to tell if a variable is either of those three?

Inductee answered 15/9, 2011 at 8:36 Comment(5)
what version of TCL do you need it for?Keos
Can you explain why do you need this in the first place? I mean, since Tcl is essentially a typeless language what you ask for looks like asking for troubles.Jaquez
@Kostix, off course. The reason I need this is to create a procedure in which a dictionary is parsed into JSON. Strings for example in JSON are surrounded by "" while integers are not. Also if the dictionary were to contain another dictionary, that dictionary should get its own JSON object within the JSON object.Inductee
@Tom, I would pick another approach then and would require the user of your package to provide the serializer with an (artifical) annotated structure using explicit type tags. Like serialize [object $mykey1 [int $value] $mykey2 [float $bar]] and so on. Trying to deduce the type of a typeless value is error prone beyond repair. Not to mention obvious cases when I want the string "0123" to be really serialized as a string, not to be interpreted as 123 or 83 (see wiki.tcl.tk/498 for the fun stuff)Jaquez
I'm confused if you have 8.5 you should have had string is list available but it isnt in the list of types you giveKeos
G
16

Tcl's variables don't have types (except for whether or not they're really an associative array of variables — i.e., using the $foo(bar) syntax — for which you use array exists) but Tcl's values do. Well, somewhat. Tcl can mutate values between different types as it sees fit and does not expose this information[*]; all you can really do is check whether a value conforms to a particular type.

Such conformance checks are done with string is (where you need the -strict option, for ugly historical reasons):

if {[string is integer -strict $foo]} {
    puts "$foo is an integer!"
}

if {[string is list $foo]} {    # Only [string is] where -strict has no effect
    puts "$foo is a list! (length: [llength $foo])"
    if {[llength $foo]&1 == 0} {
        # All dictionaries conform to lists with even length
        puts "$foo is a dictionary! (entries: [dict size $foo])"
    }
}

Note that all values conform to the type of strings; Tcl's values are always serializable.

[EDIT from comments]: For JSON serialization, it's possible to use dirty hacks to produce a “correct” serialization (strictly, putting everything in a string would be correct from Tcl's perspective but that's not precisely helpful to other languages) with Tcl 8.6. The code to do this, originally posted on Rosetta Code is:

package require Tcl 8.6

proc tcl2json value {
    # Guess the type of the value; deep *UNSUPPORTED* magic!
    regexp {^value is a (.*?) with a refcount} \
        [::tcl::unsupported::representation $value] -> type

    switch $type {
        string {
            # Skip to the mapping code at the bottom
        }
        dict {
            set result "{"
            set pfx ""
            dict for {k v} $value {
                append result $pfx [tcl2json $k] ": " [tcl2json $v]
                set pfx ", "
            }
            return [append result "}"]
        }
        list {
            set result "\["
            set pfx ""
            foreach v $value {
                append result $pfx [tcl2json $v]
                set pfx ", "
            }
            return [append result "\]"]
        }
        int - double {
            return [expr {$value}]
        }
        booleanString {
            return [expr {$value ? "true" : "false"}]
        }
        default {
            # Some other type; do some guessing...
            if {$value eq "null"} {
                # Tcl has *no* null value at all; empty strings are semantically
                # different and absent variables aren't values. So cheat!
                return $value
            } elseif {[string is integer -strict $value]} {
                return [expr {$value}]
            } elseif {[string is double -strict $value]} {
                return [expr {$value}]
            } elseif {[string is boolean -strict $value]} {
                return [expr {$value ? "true" : "false"}]
            }
        }
    }

    # For simplicity, all "bad" characters are mapped to \u... substitutions
    set mapped [subst -novariables [regsub -all {[][\u0000-\u001f\\""]} \
        $value {[format "\\\\u%04x" [scan {& } %c]]}]]
    return "\"$mapped\""
}

Warning: The above code is not supported. It depends on dirty hacks. It's liable to break without warning. (But it does work. Porting to Tcl 8.5 would require a tiny C extension to read out the type annotations.)


[*] Strictly, it does provide an unsupported interface for discovering the current type annotation of a value in 8.6 — as part of ::tcl::unsupported::representation — but that information is in a deliberately human-readable form and subject to change without announcement. It's for debugging, not code. Also, Tcl uses rather a lot of different types internally (e.g., cached command and variable names) that you won't want to probe for under normal circumstances; things are rather complex under the hood…

Geber answered 15/9, 2011 at 9:24 Comment(9)
Thanks you for the feedback, but it seems like this will not work. In the if {[string is list $foo]} { portion. Strings that hold multiple words are also seen als lists. Also if I were to have a list containing an even amount of items, this code would always classify it as a dictionary. I'm not sure if what I'm trying to do is even possible though, your code might just as well be the best possible sollution.Inductee
@Tom, that's because a string with multiple words isn't differentiated from a variable created with the 'list' command. For example, try this: set foo "ab bb dd"; lindex $foo 1Ravelin
is string is list 8.5 or 8.6?, as far as I can see it may not be available to Tom?Keos
It's in 8.5, which is the current standard production-level release. Being on 8.4 (or before!) would qualify you as being “antiquated” (or stuck in Java).Geber
@Tom: Well, all even-length lists are valid dictionaries, though possibly with less-than-ideal representations (e.g., duplicate keys).Geber
I'm beginning to suspect that the thing I'm trying to achieve (converting a dict to completely valid JSON) might not be possible in TCLInductee
For Tcl and JSON, see wiki.tcl.tk/13419 and tcllib.sourceforge.net/doc/json.htmlDemonize
@Tom: I have written a working JSON serializer for Tcl 8.6 (see rosettacode.org/wiki/JSON#Tcl for the code) but realize that it's not considered particularly good style. Good style would be to have a separate type descriptor saying what to produce; Tcl's type assumptions really don't match those of JSON (or Javascript or Python or Ruby or a number of other languages).Geber
Hmm, that JSON serializer is fragile. I can see ways in which it will break already. :-)Geber
K
8

The other answers all provide very useful information, but it's worth noting something that a lot of people don't seem to grok at first.

In Tcl, values don't have a type... they question is whether they can be used as a given type. You can think about it this way

string is integer $a

You're not asking

Is the value in $a an integer

What you are asking is

Can I use the value in $a as an integer

Its useful to consider the difference between the two questions when you're thinking along the lines of "is this an integer". Every integer is also a valid list (of one element)... so it can be used as either and both string is commands will return true (as will several others for an integer).

Kerrill answered 7/10, 2011 at 14:53 Comment(0)
P
1

If you want to deal with JSON then I highly suggest you read the JSON page on the Tcl wiki: http://wiki.tcl.tk/json.

On that page I posted a simple function that compiles Tcl values to JSON string given a formatting descriptor. I also find the discussion on that page very informative.

Pensioner answered 21/9, 2011 at 3:10 Comment(0)
K
0

For arrays you want array exists for dicts you want dict exists

for a list I don't think there is a built in way prior to 8.5?, there is this from http://wiki.tcl.tk/440

proc isalist {string} {
  return [expr {0 == [catch {llength $string}]}]
}
Keos answered 15/9, 2011 at 8:53 Comment(2)
Thanks you :) can you explain how you would use dict exists in this case? I have tried to use it like this: if {[dict exists $testData2 nullval]} { #do something } but it will return errors if the variable is not a dict.Inductee
@Tom, in that case I would wrap your if statement in a 'catch', which will allow you to process the case where the variable is not a dict.Ravelin
S
0

To determine if a variable is an array:

proc is_array {var} {
    upvar 1 $var value
    if {[catch {array names $value} errmsg]} { return 1 } 
    return 0
}   

# How to use it
array set ar {}
set x {1 2 3}
puts "ar is array? [is_array ar]"; # ar is array? 1
puts "x is array? [is_array x]";   # x is array? 0
Spicebush answered 15/9, 2011 at 15:32 Comment(3)
You're missing [array exists varname] -- tcl.tk/man/tcl8.5/TclCmd/array.htmDemonize
jk already mentioned array exists, I just want to offer a different way to do things.Spicebush
I see. Generating the list of array names (just to throw them away) is expensive: you might want array size instead.Demonize
G
0

For the specific case of telling if a value is usable as a dictionary, tcllib's dicttool package has a dict is_dict <value> command that returns a true value if <value> can act as one.

Gothic answered 19/6, 2020 at 1:31 Comment(1)
Rather belated answer, but another question was just closed as a duplicate of this one, so it seemed appropriate.Gothic

© 2022 - 2024 — McMap. All rights reserved.