unless

unless is an alternative to if, as seen in Perl.

Since Tcl 8.6, tailcall provides the most simple and flexible implementation of unless:

proc unless {test script args} {
    tailcall if !($test) $script {*}$args
}

Previous implementations:

proc unless {condition body} {
    uplevel [list if !($condition) $body]
}

example:

unless {$tcl_version >= 8.4} {
    error "package requires Tcl version 8.4 or greater"
}

(NEM notes that package versions are not real numbers, you should use package vcompare for this)

Lars H: An alternative implementation, which avoids shimmering:

proc unless {condition body} {
    uplevel 1 [list if $condition then {} else $body]
}

glennj: additionally, provide an else clause...

proc unless {cond body args} {
    set else_body {}
    if {([llength $args] == 2) && ([lindex $args 0] eq {else})} {
        set else_body [lindex $args 1]
    } elseif {[llength $args] > 0} {
        error "usage: [lindex [info level 0] 0] expr body1 ?else body2?"
    }
    uplevel 1 [list if $cond then $else_body else $body]
}

The above implementations don't work if the command is break or continue; here's a more robust solution:

proc unless {expr command} {
    global errorInfo errorCode

    set code [catch {uplevel [list if $expr {} else $command]} result]
    switch -exact -- $code {
        0           {return $result}
        1           {return -code error -errorinfo $errorInfo \
                            -errorcode $errorCode $result}
        2           {return -code return $result}
        3           {return -code break}
        4           {return -code continue}
        default     {return -code $code $result}
    }
}

NEM offers this alternative version:

set ::TCL_ERROR [catch error]
proc unless {expr command} {
    global errorInfo errorCode TCL_ERROR
    
    set code [catch {uplevel 1 [list if $expr {} else $command]} result]
    if {$code == $TCL_ERROR} {
        return -code error -errorinfo $errorInfo \
               -errorcode $errorCode $result
    } else {
        return -code $code $result
    }
}

i.e., using a global constant instead of hard-coding exception return codes and simplifying the switch (continue, return and break already set appropriate $result values). In 8.5 you can make use of extra options to catch and return:

package require Tcl 8.5
set ::TCL_ERROR [catch error]

proc unless {expr command} {
    global errorCode errorInfo TCL_ERROR
    
    set code [catch { uplevel 1 [list if $expr {} else $command] } result opts]
    if {[dict get $opts -level] == 0} {
        dict set opts -code $code
    }
    if {$code == $TCL_ERROR} {
        dict set opts -errorcode $errorCode
        dict set opts -errorinfo $errorInfo
    }
    dict incr opts -level
    return -options $opts $result
}

I think that works correctly, but it would be nice if someone more familiar with the new options could verify that that is how they are supposed to be used.


kostix: I think that all implementations above are too complicated that is contrary to the expressive power of Tcl. The implementation below is a one-liner allowing arbitrary number of else and elseif clauses. In other words, it works like built-in if but the result of the very first expression is negated. It does not, therefore, handle something like elseunless. ;-)

proc unless {expr args} {
    return -code [catch { uplevel [list if !($expr)] $args } res] $res
}

Passing of error codes up is simplified due to the fact that catch returns exactly what return uses for its -code option.

Note that this construct fails to process this:

unless {"} {puts then} else {puts else}

with the

missing "

message. This is because the command that uplevel evaluate will be

if !(\") { puts then } else { puts else }

which to me seems OK. Any explanations are welcome.

NEM: Uplevel will get \" but if then calls expr which does another round of substitution, resulting in the backslash being lost and expr seeing an unbalanced quote. You can try:

proc unless {expr args} {
    return -code [catch { uplevel [linsert $args 0 if "{!($expr)}"] } res] $res
}

Which will then produce the error that "(!")" is not a boolean expression, which it isn't. Weirder is this:

% info patchlevel
8.5a4
% if {"} { puts then } else { puts else } 

% set errorInfo

    while executing
"if {"} { puts then } else { puts else }"

Same seems to happen in 8.4.12.

kostix: Yes, Neil, I confirm that 8.4.9 and 8.4.13 both produce exactly the same result (including setting errorInfo).

Unfortunately, your version doesn't work for most cases since if one calls it like

unless {cond} ...

she gets

if {{!cond}} ...

Seems like linsert does its job too well and cares about well-formed list too much. ;-)

Also seems like my solution doesn't prevent shimmering but I don't see any obvious fix for this.

Lars H: linsert is doing The Right Thing, NEM had just put in an unnecessary pair of braces (perhaps thinking more string concatenation than list manipulation). A variant of his implementation which should avoid shimmering is:

proc unless {expr args} {
    return -code [catch { 
        uplevel 1 [linsert $args 0 if [expr {![
            uplevel 1 [list expr $expr]
        ]}]] 
    } res] $res
}

(Use uplevel 1 expr to evaluate the expression, negate it locally, and then put the negated boolean back into the upleveled if. This is getting rather silly, though.)

NEM: Without the extra braces, the linsert version is roughly equivalent to kostix's original. You could use an extra level of list-quoting, which produces a slightly different error message. However, I wasn't sure if expressions are actually compatible with lists, so I went for the safer "I just want braces round this expression" option. Kostix's first version is the correct one: an unbalanced quote should be an error.

kostix: OK, in the Tcl Chatroom, DGP recently pointed me to the registered Tcl bug #1029267. So the question about the unbalanced quote in expr may be closed.


JMN 2006-06-01:

I think these implementations somewhat miss the point of unless in Perl. I'm not a Perl user myself.. and I'm aware of the controversy of using the word 'beauty' in reference to Perl, but...

The 'beauty' IMHO of unless is that it appears after the main clause. i.e It can be used in roughly the form.

<do something>  unless <some condition>

Thus the purpose as I see it of unless in this case, is to highlight for the code-reader the 'usual case' of execution and lower the profile of the condition part of the clause. i.e it is an aid to readability. The Tcl implementations above seem to me to be mere inverted if's - which fail to enhance code readability at all.

I think something more like the following form would be more in the spirit of Perl's unless:

do {
    something
} unless condition

RS: Easily done :-)

proc do {body "unless" condition} {
    if {$unless ne {unless}} {error "usage: do body unless condition"}
    uplevel 1 [list if $condition then {} else $body]
}

PYK claps slowly and rolls eyes at RS' use of double quotes as self-documenting code in "unless".

kostix (comment on the last edit of JMN): My point of using unless is to get rid of such code:

if {!(some_expression)} ...

In other words, yes, I just want that inverted if behaviour.

As to Perlish "statement modifiers" JMN have mentioned, the if modifier is used equally often as unless by the Perl folks. And while JMN is right about the "lower profile" of the conditional part, one of the ideas for such syntax is to improve the readability of code as the natural language: "do_that unless this_holds". So I like this idea, too, and will use the RS's code snippet.

And one more point: my initial 0.5 cents on unless were inserted also to show an implementation with return -code + catch technique I have found useful. May be it should go to some other page, but I'm not sure to what exactly.

slebetman: I am a Perl user and I'd just like to clear up some confusion about Perl syntax. The implementations of unless above is actually exactly equivalent as unless in Perl - that is, unless is simply an if where the conditional expression is inverted. The syntax of

<code> if <condition>

is not the property of unless per se but a property of if in Perl. And since unless is inverted if then it is also implemented with compatible syntax.

unless ($foo) { $bar->do_something; } # is perfectly legal Perl

Since Tcl does not have such syntax for if, it would not be right to implement one just for unless. A better implementation of RS's do would be:

proc do {body if condition} {
    set TRUE {}
    set FALSE {}
    if {$if eq "if"} {
        set TRUE $body
    } else {$if eq "unless"} {
        set FALSE $body
    } else {
        error "usage: do body if/unless condition"
    }
    uplevel 1 [list if $condition $TRUE else $FALSE]
}

#usage:
do something if {$test_for_truth}
do something unless {$test_for_falsehood}

Also, as a Perl coder I'd also like to mention what is accepted as best practice in Perl related to if and unless:

  • do not use unless, it may lead to misunderstood and therefore buggy code especially for complex conditions (I've personally experienced this, many times).
  • do not use the <code> if <condition> syntax, for long bits of code and/or complex conditions it leads to hard to read code.

So to me Tcl got it right by neither implementing unless nor the if-after-body syntax to begin with ;-)