TIP 342: Dict Get With Default

Login
Bounty program for improvements to Tcl and certain Tcl packages.
Author:		Lars Hellström <Lars.Hellstrom@residenset.net>
State:		Final
Type:		Project
Vote:		Done
Tcl-Version:	8.7
Created:	27-Nov-2008
Keywords:	dictionary, default value
Post-History:	
Tcl-Branch:     tip-342
Votes-For:      DKF, JN, DGP, SL, AK
Votes-Against:  none
Votes-Present:  FV

Abstract

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

Specification

The dict command will get a new subcommand

dict getwithdefault dictionary key ?key ...? value

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 keys 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 keys 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.

To facilitate easy use by lazy programmers, it will be aliased as dict getdef.

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 {
    foo bar
} else {
    foo $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.

An alternate version with try fixes some of the problems... but is ugly:

try {
    set value [dict get $D -apa]
} trap {TCL LOOKUP DICT} {} {
    set value bar
}
foo $value

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 keys 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, in patch #2370575.

Alternate Names

The following alternate names were considered for getwithdefault during the TIP voting period:

  • getdef — selected.
  • default — feels like it should address the topics of defaults more widely.
  • fetch — consensus is that this should be a command with a different return signature if/when such is created (e.g., a boolean to say whether the value was found).
  • get? — rejected as Tcl's own commands (mostly) do not use non-alpha characters in their names, leaving more space for user code.

Copyright

This document has been placed in the public domain.

History