When dictionaries were first implemented and added to Tcl, why was the dict get command implemented in a way that allows an error to occur if an attempt is made to retrieve a value for a key that is not present in the dictionary?
This requires you to wrap the command in a catch statement every time you use it if you want to ensure that it is completely safe. It always seemed to me that a frequently used command like this would have some sort of exception handling built in.
It's a common design choice in Tcl (as well as some other languages). When a command like dict get (or, more commonly, open) fails, the program has to deal with it, which means it has to be alerted to the failure in some way.
The most common options is to have the failing command either
Return an out-of-domain value (such as null in languages that have it), or
Raise an exception.
(E.g. the lsearch command either returns an index value if successful and -1 if it fails (the first option). The dict get command either returns a value if successful and raises an exception if it fails (the second option).)
The first option isn't really practicable for dict get command, since there is no out-of-domain value. Any Tcl value could possibly be stored in a dictionary, so you can't look at the result of dict get and know that it has failed to find a value. The empty string is often used as a pseudo-null value in Tcl, but it's quite likely that empty strings are actual values in a dictionary.
So dict get raises an exception when it fails. It's not so bad. Exceptions have a lot of neat properties, such as taking control directly to the nearest enclosing handler regardless of how many stack levels it has to unwind.
(It's not really possible to handle all exceptions inside the command: a handler must know how to deal with the error, and dict get can't know that.)
Either way, a command that can fail needs to be wrapped in some kind of check. If the foo command is used to get a resource that might not be available and there is no sensible default, the code calling it must look either like this:
if {[set x [foo]] ne {BAD_RETURN_VALUE}} {
# use the resource
} else {
# deal with failure
}
or like this:
try {
foo
} on ok x {
# use the resource
} on error {} {
# deal with failure
}
or like this (if a predicate function predicting if foo will succeed exists):
if {[foo-will-succeed]} {
set x [foo]
# use the resource
} else {
# deal with failure
}
Which is about as much bother in each of the cases. Since out-of-domain values are rare in Tcl and error handling is so versatile, the predicate or exception strategies are usually favored.
patthoyts has already showed one way to add a error-suppressing getter function to the dict ensemble. Another relatively lightweight invocation is
set foo [try {dict get $bar xyzzy} on error {} {}]
which returns the result of the dict get call if successful and the empty string if not, and squashes any errors raised.
set foo [try {dict get $bar xyzzy} on error {} {return 42}]
This invocation sets a default return value to use on failure.
If the invocation is still bothersome, it can be made into a command:
proc dictget args {
set default {}
if {[lindex $args 0] eq {-default}} {
set args [lassign $args - default]
}
try {
dict get {*}$args
} on error {} {
set default
}
}
The synopsis for this is
dictget ?-default value? ?dictionaryValue? ?key ...?
Documentation: dict, if, proc, return, set, try
The dict command is implemented as an ensemble. This means you can very easily extend it yourself to achieve this. I like to call this dict get? and have it return an empty value if the key does not exist. We can add this new subcommand as follows:
proc ::tcl::dict::get? {dict key} {
if {[dict exists $dict $key]} {
return [dict get $dict $key]
}
return
}
namespace ensemble configure dict \
-map [linsert [namespace ensemble configure dict -map] end get? ::tcl::dict::get?]
As you can see this trivially wraps up the dict exists call with the dict get call but presents it as a builtin part of the dict command due to the ensemble update. In use it looks like this:
if {[dict get? $meta x-check-query] eq "yes"} {
... do stuff ...
}
(This can be seen in action in the Tcl test suite httpd test server code.)
I guess that is why we are provided with the dict exists command.
You might be expecting dict get to return empty string of that key element doesn't exists. But, having implementation like them will cause problem if the actual value of any key itself is an empty string.
% set demo {id {} name Dinesh}
id {} name Dinesh
% dict get $demo id
% dict get $demo age
key "age" not known in dictionary
%
Use dict exists if you want to skip catch.
Related
For some Perl diagnostic tests, I'm recording assorted bits of information formatted as JSON using JSON::MaybeXS.
I get an error when I want to record the current Perl version, which I obtain from the special variable $^V.
As the minimal demonstration script shows, the error occurs unless I quote $^V as "$^V".
json_perl_version_test.pl
#!/usr/bin/env perl
use strict;
use warnings;
use v5.18;
use JSON::MaybeXS;
say "Running Perl version $^V";
my $item = 'Wut?';
my %hash1 = (
something => $item,
v_unquoted => $^V
);
eval { say say 'Hash1: ', encode_json \%hash1 };
say "Oops - JSON encode error: $#" if $#;
my %hash2 = (
something => $item,
v_quoted => "$^V"
);
say 'Hash2: ', encode_json \%hash2;
# Running Perl version v5.34.0
# Oops - JSON encode error: encountered object 'v5.34.0',
# but neither allow_blessed, convert_blessed nor allow_tags
# settings are enabled (or TO_JSON/FREEZE method missing) at
# /Users/bw/Documents/Dev/tests/json_perl_version_test.pl line 17.
# Hash2: {"something":"Wut?","v_quoted":"v5.34.0"}
Note that it wasn't necessary to quote $item.
The error message refers to some ways to handle other cases, but seemingly not including canonical Perl version dotted-decimal strings. I've looked through the main Perl JSON modules (recent versions of JSON::MaybeXS, JSON, and Cpanel::JSON::XS), but can't find anything referring to $^V or dotted-decimal strings. Also don't find a relevant question on SO :(.
Perhaps I'm missing something? Or am I stuck with needing to quote $^V?
Reasons?
Thanks,
The $^V variable is really an object
The revision, version, and subversion of the Perl interpreter, represented as a version object.
An object cannot be stored in JSON just so. Quoting it stringifies it.
It is possible to make JSON::XS (and its Cpanel::) take a blessed reference but it involves more work. See Object Serialization. The cleanest complete solution is with convert_blessed, when the encode method will look for a TO_JSON method (in the class of the object that is to be added to JSON), which would return a JSON-ready string.
Alas, there is no such a thing for the version (nor for a few other classes I tried, like DateTime). One can add that method to the class† but just thinking of it makes quotes look nice.
Another way is to get explicit in making the version object stringify
my $json = JSON::XS->new->encode( { ver => $^V->stringify } )
This is yet more elaborate but at least now it's clear what the matter is, without magic quotes.
Or just quote it and add a comment.
† By "monkey-patching" it, for example
add the sub, with a fully qualified name
perl -Mstrict -wE'use JSON::XS; say $^V;
sub version::TO_JSON { return $_[0]->stringify };
my $json = JSON::XS->new->convert_blessed->encode( { ver => $^V } );
say $json'
Can also add a sub via string eval at runtime but that doesn't seem needed.
write the code reference to the class's symbol table at runtime
perl -wE'use JSON::XS; say $^V;
*{"version"."::"."TO_JSON"} = sub { return $_[0]->stringify };
$json = JSON::XS->new->convert_blessed->encode( { ver => $^V } );
say $json'
well, or really with strict in effect we need to allow the symbolic refs
perl -Mstrict -wE'use JSON::XS; say $^V;
NO_STRICT_REFS: {
no strict "refs";
my ($class, $method) = qw(version TO_JSON);
*{$class."::".$method} = sub { return $_[0]->stringify }
};
my $json = JSON::XS->new->convert_blessed->encode( { ver => $^V } );
say $json'
where I also added variables for class name and method, not necessary but better.
This is packaged for far nicer use in Sub::Install
use Sub::Install;
Sub::Install::install_sub({
code => sub { ... }, into => $package, as => $subname
});
There are expected defaults and a bit more in the module.
Or, of course by writing a wrapper class or some such but that's something yet else.
Blessed Perl objects can't be stored in JSON without extra steps (mentioned by the error).
print ref $^V; # version
A possible workaround:
my $j = Cpanel::JSON::XS->new->convert_blessed; # Allow stringification.
say 'Hash1: ', $j->encode(\%hash1);
I am parsing some JSON which has been converted to Ruby data structures. I have records that look either like this:
"uros"=>[{"ure"=>"zip*less", "prs"=>[{"mw"=>"ˈzip-ləs", "sound"=>{"audio"=>"ziples01", "ref"=>"c", "stat"=>"1"}}]
or like this:
"uros"=>[{"ure"=>"gas chromatographic", "fl"=>"adjective"}]
I want to pull out the hash value for the key audio when it appears.
What is the clean way to test for the presence of said key in Ruby? Here are my attempts thus far. The variable entry represents the enclosing data structure:
if entry["uros"] != nil # Nope
#if entry["uros"]["prs"] != nil # Nope
#if not entry["uros"]["prs"]["sound"]["audio"].first.instance_of? nil # Nope
#if ! entry["uros"]["prs"].instance_of? nil # Nope
puts entry["uros"].first["prs"].first["sound"]["audio"]
end
The error message I get is either:
undefined method `first' for nil:NilClass (NoMethodError)
or conversely:
undefined method `[]' for nil:NilClass (NoMethodError)
How would you do this?
Hash has a method for checking presence of a key-value pair
if entry.key?('uros')
But problem with your code is you don't check for existence of the nested keys. You check for "uros", but then "prs" might not exist.
Here's a better version that allows nils at every step
audio = entry.dig('uros', 0, 'prs', 0, 'sound', 'audio')
You can use the safe navigation operator to search around the the data structure without getting a bunch of NoMethodErrors. The safe navigation operator will call the method, or return nil if called on nil.
audio = entry["uros"]
&.first
&.[]("prs")
&.first
&.dig("sound", "audio")
This might look funny, but remember that [] is just another method. If any part of that method chain returns nil, the whole thing will return nil.
entry["uros"]&.first is roughly equivalent to...
entry["uros"].first if entry["uros"] != nil
Run cpanm --look DBIx::Class ; cd examples/Schema/ to use the example database.
use 5.024;
use strictures;
use JSON::MaybeXS qw(encode_json);
use MyApp::Schema qw();
use Sub::Install qw();
my $s = MyApp::Schema->connect('dbi:SQLite:db/example.db');
# Yes, I know Helper::Row::ToJSON exists.
Sub::Install::install_sub({
code => sub {
my ($self) = #_;
return { map {$_ => $self->$_} keys %{ $self->columns_info } };
},
into => $s->source('Track')->result_class,
as => 'TO_JSON',
});
my ($t) = $s->resultset('Cd')->first->tracks;
say ref $t->can('TO_JSON'); # 'CODE', ok
say ref $t->TO_JSON; # 'HASH', ok
say encode_json $t;
# encountered object 'MyApp::Schema::Result::Track=HASH(0x1a53b48)',
# but neither allow_blessed, convert_blessed nor allow_tags settings
# are enabled (or TO_JSON/FREEZE method missing) at …
I expect the serialiser to find the installed hook and use it, but instead I get the error above. What's going wrong?
In order to make JSON::XS consider TO_JSON, you have to explicitly enable convert_blessed option:
my $coder = JSON::XS->new;
$coder->convert_blessed(1);
say $coder->encode($t);
According to docs:
$json = $json->convert_blessed ([$enable])
$enabled = $json->get_convert_blessed
See "OBJECT SERIALISATION" for details.
If $enable is true (or missing), then encode, upon encountering a blessed object, will check for the availability of the TO_JSON method
on the object's class. If found, it will be called in scalar context
and the resulting scalar will be encoded instead of the object.
The TO_JSON method may safely call die if it wants. If TO_JSON returns other blessed objects, those will be handled in the same way.
TO_JSON must take care of not causing an endless recursion cycle (==
crash) in this case. The name of TO_JSON was chosen because other
methods called by the Perl core (== not by the user of the object) are
usually in upper case letters and to avoid collisions with any to_json
function or method.
If $enable is false (the default), then encode will not consider this type of conversion.
This setting has no effect on decode.
(emphasis mine)
I am using PowerShell v3 and the Windows PowerShell ISE. I have the following function that works fine:
function Get-XmlNode([xml]$XmlDocument, [string]$NodePath, [string]$NamespaceURI = "", [string]$NodeSeparatorCharacter = '.')
{
# If a Namespace URI was not given, use the Xml document's default namespace.
if ([string]::IsNullOrEmpty($NamespaceURI)) { $NamespaceURI = $XmlDocument.DocumentElement.NamespaceURI }
# In order for SelectSingleNode() to actually work, we need to use the fully qualified node path along with an Xml Namespace Manager, so set them up.
[System.Xml.XmlNamespaceManager]$xmlNsManager = New-Object System.Xml.XmlNamespaceManager($XmlDocument.NameTable)
$xmlNsManager.AddNamespace("ns", $NamespaceURI)
[string]$fullyQualifiedNodePath = Get-FullyQualifiedXmlNodePath -NodePath $NodePath -NodeSeparatorCharacter $NodeSeparatorCharacter
# Try and get the node, then return it. Returns $null if the node was not found.
$node = $XmlDocument.SelectSingleNode($fullyQualifiedNodePath, $xmlNsManager)
return $node
}
Now, I will be creating a few similar functions, so I want to break the first 3 lines out into a new function so that I don't have to copy-paste them everywhere, so I have done this:
function Get-XmlNamespaceManager([xml]$XmlDocument, [string]$NamespaceURI = "")
{
# If a Namespace URI was not given, use the Xml document's default namespace.
if ([string]::IsNullOrEmpty($NamespaceURI)) { $NamespaceURI = $XmlDocument.DocumentElement.NamespaceURI }
# In order for SelectSingleNode() to actually work, we need to use the fully qualified node path along with an Xml Namespace Manager, so set them up.
[System.Xml.XmlNamespaceManager]$xmlNsManager = New-Object System.Xml.XmlNamespaceManager($XmlDocument.NameTable)
$xmlNsManager.AddNamespace("ns", $NamespaceURI)
return $xmlNsManager
}
function Get-XmlNode([xml]$XmlDocument, [string]$NodePath, [string]$NamespaceURI = "", [string]$NodeSeparatorCharacter = '.')
{
[System.Xml.XmlNamespaceManager]$xmlNsManager = Get-XmlNamespaceManager -XmlDocument $XmlDocument -NamespaceURI $NamespaceURI
[string]$fullyQualifiedNodePath = Get-FullyQualifiedXmlNodePath -NodePath $NodePath -NodeSeparatorCharacter $NodeSeparatorCharacter
# Try and get the node, then return it. Returns $null if the node was not found.
$node = $XmlDocument.SelectSingleNode($fullyQualifiedNodePath, $xmlNsManager)
return $node
}
The problem is that when "return $xmlNsManager" executes the following error is thrown:
Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Xml.XmlNamespaceManager".
So even though I have explicitly cast my $xmlNsManager variables to be of type System.Xml.XmlNamespaceManager, when it gets returned from the Get-XmlNamespaceManager function PowerShell is converting it to an Object array.
If I don't explicitly cast the value returned from the Get-XmlNamespaceManager function to System.Xml.XmlNamespaceManager, then the following error is thrown from the .SelectSingleNode() function because the wrong data type is being passed into the function's 2nd parameter.
Cannot find an overload for "SelectSingleNode" and the argument count: "2".
So for some reason PowerShell is not maintaining the data type of the return variable. I would really like to get this working from a function so that I don't have to copy-paste those 3 lines all over the place. Any suggestions are appreciated. Thanks.
What's happening is PowerShell is converting your namespace manager object to a string array.
I think it has to do with PowerShell's nature of "unrolling" collections when sending objects down the pipeline. I think PowerShell will do this for any type implementing IEnumerable (has a GetEnumerator method).
As a work around you can use the comma trick to prevent this behavior and send the object as a whole collection.
function Get-XmlNamespaceManager([xml]$XmlDocument, [string]$NamespaceURI = "")
{
...
$xmlNsManager.AddNamespace("ns", $NamespaceURI)
return ,$xmlNsManager
}
More specifically, what is happening here is that your coding habit of strongly typing $fullyQualifiedModePath is trying to turn the result of the Get (which is a list of objects) into a string.
[string]$foo
will constrain the variable $foo to only be a string, no matter what came back. In this case, your type constraint is what is subtly screwing up the return and making it Object[]
Also, looking at your code, I would personally recommend you use Select-Xml (built into V2 and later), rather than do a lot of hand-coded XML unrolling. You can do namespace queries in Select-Xml with -Namespace #{x="..."}.
please assist me in this ,,,
I have a tcl array called all_tags ,, but the thing is i need to convert it into a javascript array in my page but i am weak when it comes to javascript .
please advise me if below is correct and if not ,,what is the right way ?
<script>
var mytags = new Array();
<%
foreach tag $all_tags {
ns_puts [subst {
mytags.push('$tag');
}]
}
%>
</script>
and afterwards is it possible to use my javascript array in a tcl proc ?
To turn data in Tcl into JSON, you want the json::write package from Tcllib. You'd use it like this to make a JSON object from a Tcl array (and a similar approach works for Tcl dictionaries):
package require json::write
set accumulate {}
foreach {key value} [array get yourArray] {
lappend accumulate $key [json::write string $value]
}
set theJsonObject [json::write object {*}$accumulate]
To turn a Tcl list into a JSON array:
package require json::write
set accumulate {}
foreach item $yourList {
lappend accumulate [json::write string $value]
}
set theJsonArray [json::write array {*}$accumulate]
Note in these two cases I've assumed that the values are all to be represented as JSON strings. If the values to embed are numbers (or true or false) you don't need to do anything special; the values as Tcl sees them work just fine as JSON literals. Embedding lists/arrays/dicts takes “recursive” use of json::write and a bit more planning — it's not automatic as Tcl and JSON have really very different concepts of types.