342.md at [331f6d28dd]

Login

File tip/342.md artifact 1f3ce25bc3 part of check-in 331f6d28dd


# TIP 342: Dict Get With Default
	Author:		Lars Hellström <[email protected]>
	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 _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.

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 _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, in [patch #2370575](https://core.tcl-lang.org/tcl/tktview/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.