Version 13 of Getting a value or a default value

Updated 2014-04-01 20:21:59 by EMJ

Peter Lewerin (2013-12-26): I can't believe this basic trick hasn't been described here yet, but I can't find it even after repeated searches. I'll put it up here, and if it's already on the wiki somewhere, let me know and I'll nominate this page for deletion.

A common idiom in programming is "use this value unless it isn't what I want to have; in that case use this default value". For instance, when assigning a value to a variable, it can be useful to make sure that the variable gets some safe value instead if the intended value isn't up to snuff.

For me, it's often "I'd like to use $a unless it's an empty string, in which case I'll use $b". So I have this little proc which I tend to copy into my projects (as you can see, it's possible to have some value other than an empty string as the undesired value; actually I have never used that option except when testing the command):

proc either {value default {nullval {}}} {
    expr {$value ne $nullval ? $value : $default}
}

# use like this:

set foo {}
either $foo Tcl
# -> Tcl

set foo xxx
either $foo yyy
# -> xxx

either $foo yyy xxx
# -> yyy

A variant of this is to define the unwanted value as any logically false value (0, false, or no in Tcl): this works like the or operator (however it is written) in some languages (like Scheme's or or Perl's ||) but not as in other languages which either 1) don't accept non-boolean values as operands to or (Tcl doesn't), or 2) always has the or operator return a boolean value, or both.

proc eitherOr {value default} {
    expr {$value ni {0 "false" "no"} ? $value : $default}
}

set foo xxx
eitherOr $foo yyy
# -> xxx

set foo no
eitherOr $foo yyy
# -> yyy

Another variant is to instead of an unwanted value use however the language defines the undefined state. Some languages have operators (known as null coalescing operators) for this, such as C#'s ?? or Perl's //; Scheme's or works here too. Tcl doesn't have a value for undefined / null, but at least we can test a variable for existence:

proc eitherExists {varname default} {
    upvar $varname _varname
    expr {[info exists _varname] ? $_varname : $default}
}

set foo xxx
eitherExists foo yyy
# -> xxx

unset foo
eitherExists foo yyy
# -> yyy

Finally (?) we can use a provided predicate function to tell us whether to use the value or the default value. Note that the function must be an actual predicate, i.e. return a legal boolean value (1, true, yes, 0, false, or no).

proc eitherApply {value default func} {
    expr {[apply $func $value] ? $value : $default}
}

set foo xxx
eitherApply $foo yyy {{val} {string match x* $val}}
# -> xxx

set foo aaa
eitherApply $foo yyy {{val} {string match x* $val}}
# -> yyy

This is of course the most generic variant. Actually it can be used to redefine the original command:

proc either {value default {func {{val} {expr {$val ne {}}}}}} {
    expr {[apply $func $value] ? $value : $default}
}

set foo {}
either $foo Tcl
# -> Tcl

set foo xxx
either $foo yyy
# -> xxx

either $foo yyy {{val} {expr {$val ne "xxx"}}}
# -> yyy

aspect: thanks for refreshing this - I love the name [either] and I'm definitely stealing that! Not so sure about the subcommands .. they might be a good case for sprucing up with options and arguments. A similar pattern I've been using a lot lately but irritatingly can't find a good name for is:

proc ifnotempty {_var args} {
    upvar 1 $_var var
    if {[info exists var] && $var ne ""} {
        uplevel 1 {*}$args
    }
}

I've considered [maybe], [iff] (which might be more like an error-suppressing if) .. but nothing quite fits.

PL: the term iff is usually understood as meaning "if and only if", so I wouldn't use that. Maybe ifhas?

aspect: Oops - I mis-copied the code. With the {*} hopefully it makes more sense, and makes the comment I deleted redundant.

(PL: I'm putting it back inside a discussion tag for the time being, we'll delete it later when the discussion is over.)

No, the {*} doesn't necessarily make very much more sense. The uplevel in effect already applies an implied {*} to $args (just like eval does), so what you get is a double expansion of the argument list. This means that the user can now either provide a script as a sequence of arguments:

ifnotempty foo puts Foo ; puts Bar

or as a script body:

ifnotempty foo { puts Foo ; puts Bar } ;# could've been on separate lines

or both (the semicolon seems out of place, but is necessary here):

ifnotempty foo { puts Foo ; } puts Bar

While this kind of freedom might seem like a nice thing (I suspect the Perl crowd would be all over it ;)), ultimately it will, I think, be confusing to the user. My personal preference is to make the code unambiguous in what it accepts. The uplevel 1 $args form accepts a sequence of arguments but not a script body, the uplevel 1 $body form accepts the latter but not the former (well, it does accept single-word command invocations with or without braces, such as pwd), the uplevel 1 {*}$args form accepts both and a mixture of both. I would go with the second or first form, but not with the third.

aspect - well said. I will point out that the behaviour with {*}$args more closely mimics Tcl core commands, including uplevel. I've always found this "implicit concat" behaviour a bit unnerving (see also: after, eval, ...). I can see how it served to make command/script composition less ugly in the absence of {*}, but I always find myself second-guessing where to wrap arguments with list -- and sometimes I get it horribly wrong, as in this example :-).

Hm, I just searched for implicit concat and found no obvious hits. This discussion almost warrants a page of its own ..

PL: actually, I think after, eval, and uplevel are the only ones that do concat-and-evaluate. catch doesn't, for instance (the command syntax would need a major rewrite to make that possible). There are lots and lots of control commands that take singular script arguments, however.

I also thought of another point earlier but forgot to mention it. If we rename the different versions ine1 (your original version, with uplevel 1 $args), ine2 (my version, with uplevel 1 $script), and ine3 (your later version, with uplevel 1 {*}$args), and set the command to print a) the value of foo, and b) the value of msg (which is the text "foo bar"):

% set foo bar
# -> bar
% set msg "foo bar"
# -> foo bar

% ine1 foo puts $foo
# => foo
% ine1 foo puts $msg
# => foo bar

% ine2 foo puts $foo
# =2> wrong # args: should be "ine2 _var body"
% ine2 foo { puts $foo }
# => foo
% ine2 foo { puts $msg }
# => foo bar

% ine3 foo puts $foo
# => foo
% ine3 foo puts $msg
# =2> can not find channel named "foo"

I'd say that it's not quite obvious what's wrong with the last invocation (and why the next-to-last invocation is ok) unless you are familiar with the implementation of the command (the problem with the first invocation of ine2, in contrast, is fairly obvious even from the automatically generated error message). Again, the ine1 and ine2 commands are internally consistent, while the ine3 command seems inconsistent (of course, it isn't really, but seeming inconsistency is almost as bad as real inconsistency).

  (PL's earlier suggestion)

Also, you might possibly want to consider having the body to be executed as a single argument instead of collecting the rest of the arguments, like this:

proc ifnotempty {_var body} {
    upvar 1 $_var var
    if {[info exists var] && $var ne ""} {
        uplevel 1 $body
    }
}

% ifnotempty foo {
    puts Foo
    puts Bar
}

This has three advantages: 1) it separates the variable from the code, 2) it looks more like other similar Tcl control structures, and 3) it lets the user provide several commands without having to cram them into one line with semicolons in between.

Just a thought. It's not in any way a necessary change: the command certainly works as you defined it.


AMG: As I wrote on the [append] page, [append] can be used to set a variable to empty string if it doesn't already exist, leaving its value alone otherwise. This is often all that's needed. Just do:

append var ""

As a bonus, this returns the variable's value (or empty string if it didn't exist), so just write [append var ""] instead of $var or [set var].

PL: that would indeed equivalent to using eitherExists above with the invocation eitherExists var "". It doesn't handle any of the other cases, though.