Tcl Source Code

Artifact [11b0722261]
Login

Artifact 11b07222618420c3e7b7d434083d194981fbb654:

Attachment "dict-getwithdefault.tip" to ticket [2370575fff] added by lars_h 2008-12-02 20:40:11.
TIP:            
Title:          Dict Get With Default
Version:        
Author:         Lars HellstrĀšm <Lars dot Hellstrom at residenset dot net>
State:          Draft
Type:           Project
Vote:           
Tcl-Version:    8.6
Created:        27-Nov-2008
Keywords:       dict, dictionary, default value
Post-History:   

~ Abstract

A new subcommand of '''dict''' is proposed, which returns a dictionary 
value if it exists and returns a per-call default otherwise. 


~ Specification

The '''dict''' command will get a new subcommand

 > '''dict getwithdefault''' ''dictionary'' ''key'' ?''key'' ...?
   ''value''

(I consider the name of this subcommand very much open for discussion) 
which modulo error messages behaves like

| proc dict_getwithdefault {D args} {
|     if {[dict exists $D {*}[lrange $args 0 end-1]]} then {
|         dict get $D {*}[lrange $args 0 end-1]
|     } else {
|         lindex $args end
|     }
| }

i.e., it returns the value from the ''dictionary'' corresponding to the 
sequence of ''key''s if it exists, or the default ''value'' otherwise. As 
with '''dict exists''', it is OK (and will cause the default ''value'' to 
be returned) if one of the ''key''s is missing from its dictionary, but 
an error is thrown if this path of keys cannot be traversed because the 
value associated with the previous key is not a dictionary.


~ Rationale

It is clear that getting a value from a dictionary if it exists and using 
a default otherwise is a common operation, but it is also clear that this 
can be carried out with a combination of existing Tcl commands. Hence the 
issue is whether a new subcommand for this improves efficiency and 
convenience of this operation enough to justify the possible bloat it 
brings.


~~ Alternative Methods

One approach that has been suggested for providing default values is to 
combine '''dict get''' with '''dict merge''', like so:

|  dict get [dict merge {-apa bar} $D] -apa

This approach is however appropriate mainly in situations where several 
keys are given fixed defaults simultaneously. Compared to 
'''dict getwithdefault''', it has the following disadvantages:

   * It cannot be used for keys in nested dictionaries.

   * It takes time proportional to the size of the dictionary, even 
     when only one value is inspected. Since '''dict filter key''' 
     has an optimisation for this kind of situation, there are 
     apparently maintainers which consider such differences relevant.

   * The "one '''dict merge''' early providing defaults for all keys" 
     approach cannot deal with keys that have dynamic defaults, e.g. 
     that the default for option -foo is the effective value of option
     -bar.

Hence although '''dict merge''' is sometimes appropriate for providing 
defaults, it is not a universal solution.

The basic approach is instead to, as in the '''dict_getwithdefault''' 
proc above, first use '''dict exists''' and then '''dict get''' if the 
value existed. Problems with this approach are:

   * It is redundant: already '''dict exists''' retrieves the value, 
     but doesn't return it, so '''dict get''' has to look it up all 
     over again.

   * It is bulky: if the value in dictionary ''D'' of option '''-apa''' 
     (or its default '''bar''') is to be passed as an argument to the 
     command '''foo''', then the complete command is

|     foo [if {[dict exists $D -apa]} then {dict get $D -apa}\
|       else {return -level 0 bar}]

   > or 

|     foo [expr {[dict exists $D -apa] ? [dict get $D -apa] : "bar"}]

   > which many programmers would find objectionable. The
     '''dict getwithdefault''' counterpart is merely

|     foo [dict getwithdefault $D -apa bar]

The only way to avoid the redundance of an extra look-up seems to be to 
combine '''dict get''' with '''catch''', like so:

|  if {[catch {dict get $D -apa} value]} then {set value bar} else {set value}

but this has the disadvantage of hiding other sources of error, such as 
''D'' not being a dictionary in the first place. This kind of error in 
a normal processing path is also considered poor style by some.


~~ Implementation Choices

Even if it is deemed appropriate to have a dedicated subcommand of 
'''dict''' for this, it could be argued that it needn't be part of the 
compiled Tcl core; since '''dict''' is an ensemble, anyone can extend it 
at the script level and "the core can do without this bloat". However, it 
turns out than an in-core implementation is very easy whereas the 
alternatives are not so easy.

Concretely, the necessary DictGetWithDefaultCmd is a trivial modification 
of DictExistsCmd, to take one extra argument after the ''key''s and change 
the final

|  Tcl_SetObjResult(interp, Tcl_NewBooleanObj(valuePtr != NULL));

to

|  Tcl_SetObjResult(interp, valuePtr != NULL ? valuePtr : objv[objc-1]);

It is nowhere near as easy to do this in a well-behaved extension, since 
DictExistsCmd relies on TclTraceDictPath to do most of the work, and 
the latter is AFAICT at best available in the internal stubs table.

A script-level implementation is certainly possible, but the minute 
details of producing core-looking error messages in this case appears 
considerable both compared to the functional parts of the command and 
compared to the amount of code needed to do it in the core.


~ Reference Implementation

An implementation is provided on SF, in patch #2370575.


~ Copyright

This document has been placed in the public domain.