Version 14 of unless

Updated 2006-05-31 05:56:38 by kostix

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

 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 Also seems like this solution doesn't prevent shimmering but I don't see any obvious fix for this.


Category Command - Category Control Structure