tags:

views:

159

answers:

4

Hi,

In Tcl 8.5 I can do something like this:

apply llength { 1 2 3 }

But that apply is not defined in v8.4.

How would I define apply using Tcl in v8.4?

I need this because I am converting some lisp code to Tcl. The lisp code has some constructs that I would like to port like this:

array set levels {
  TRACE  0
  DEBUG  1
  INFO   2
  WARN   3
  ERROR  4
}

set LOG_LEVEL INFO
proc setLogLevel { level } {
  global LOG_LEVEL

  set LOG_LEVEL $level
}

proc log { tag msg args } {
  global levels
  global LOG_LEVEL

  # Filter out any messages below the logging severity threshold.
  if { $levels($LOG_LEVEL) <= $levels($tag) } then {
    apply format $msg $args
  }
}

proc logTrace { msg args } {
  apply log TRACE $msg $args
}
proc logDebug { msg args } {
  apply log DEBUG $msg $args
}
proc logInfo { msg args } {
  apply log INFO $msg $args
}
proc logWarn { msg args } {
  apply log WARN $msg $args
}
proc logError { msg args } {
  apply log ERROR $msg $args
}

# Close solution (not quite correct)
proc apply {func args} {
  eval [list $func] $args
}

# Example usage:
set instName "myInst"
set viewName "myView"
set cellName "myCell"
logError "Divide by zero."

# Filtered message:
logTrace "Evaluating callbacks for instance %s." $instName
# Enable that same message
setLogLevel TRACE
logTrace "Evaluating callbacks for instance %s." $instName

# This one fails with apply definition given here
logInfo "Opening cellView %s@%s." $viewName $cellName

Thanks.

-William

A: 

Surprisingly enough, it's called apply in Tcl, as well.

Pavel Minaev
Keep in mind though, the [apply] command is only available in Tcl 8.5 and newer...
Jeff Godfrey
That's not quite working, the $args is not split out and is passed as arg1 to apply.
WilliamKF
Plus I'm on 8.4, so that is not an option.
WilliamKF
Which explains why it didn't work properly for me!
WilliamKF
A: 

I’m pretty sure your solution is going to have to use eval, and maybe uplevel too if you want it to work on procs that use uplevel or upvar.

Kragen Javier Sitaker
A: 

Short answer:

If you have the command name in a variable, you can run it by placing the variable as the first word of the line:

set mycommand puts
$mycommand "hello world"

Longer answer:

You have arguments you want expanded, without breaking the edge cases for your command, so you can use eval to "reparse" the line once you create it. Basically, you can use "eval" to expand all the arguments, then use "list" to protect certain ones from expansion

% proc {my llength of args} {args} { return [llength $args] }
% set mycommand {my llength of args}
% set args "1 2 3"
% eval $mycommand $args ;# Expands the command, so may blow up
ambiguous command name "my": {my llength} {my llength of args}
% eval [list $mycommand] [list $args] ;# protect the args, so it's not expanded, not what you want
1
% eval [list $mycommand] $args ;# Protect the things you don't want expanded (command named), but allow the args to be expanded to individual arguments
3
RHSeeger
+3  A: 

Based on your response to my comments to the original question, I recommend you learn to use eval properly instead of trying to create an apply function that works the way you think it should. My reasoning is that if you don't understand eval you don't have enough knowledge to understand how to create and use the apply command.

Believe it or not, your implementation of the apply command is more-or-less correct, but you were using it incorrectly. To describe how and why to use it properly is not worth the trouble when there are other ways to solve the problem.

Your problem boils down to this: you're given a function and N arguments, and you need a way to call that function with exactly N arguments. The proper solution for that is to use eval.

Here's how I would rewrite your log function. I took the liberty of adding code to actually print out the result rather then compute it and return it like your code did. I also added code to print out the error level:

proc log { tag msg args } {
  global levels
  global LOG_LEVEL

  # Filter out any messages below the logging severity threshold.
  if { $levels($LOG_LEVEL) <= $levels($tag) } then {
      set result [eval format \$msg $args]
      puts "$LOG_LEVEL: $result"
  }
}

Some important points to understand here. First, the word 'args' is special, and means that all additional arguments are collected into a list. So, whether you call log with zero arguments, one argument, or N arguments, args is a list and will always be a list, even if it's a list of zero or one values.

As you've discovered, the format command (potentially) needs N arguments rather than a list of N arguments. The way around this in Tcl is to use the eval statement. The simplistic explanation is that eval causes a line to be parsed twice.

This is good for $args in that it effectively removes one level of "listness" -- what was a list of N items becomes N distinct items. However, you don't want $msg to be parsed twice because it's not a list of N items. That is why there's a backslash in front of the $ -- it hides the dollar sign from the first pass of the parser. Some people prefer [list $msg], and there are other ways to accomplish the same task.

(note that in this specific case with this specific code, there's no problem in $msg getting parsed twice. It's good practice to always protect things you don't explicitly want expanded when using eval, for reasons not worth getting into here).

Next, we have to turn our attention to the other log functions. They work similarly, and need a similar treatment. These are all essentially pass-through commands, adding one extra argument. Here's how logInfo should look, again using eval:

proc logInfo {msg args} {
    eval log INFO \$msg $args
}

Again notice that $msg has a backslash in front of it. This is for the same reason as above -- we want the extra round of parsing for $args but not for $msg.

With those two changes, your code works.

However, there's an arguably better way to implement the logX functions. Since all you're doing is adding an extra argument and then passing everything else as-is to the log function you can take advantage of the interpreter's ability to create aliases. For example:

interp alias {} logTrace {} log TRACE
interp alias {} logDebug {} log DEBUG
interp alias {} logInfo {} log INFO
interp alias {} logWarn {} log WARN
interp alias {} logError {} log ERROR

In the above code, the curly braces simply mean "in the current interpreter". Tcl has the ability to have multiple interpreters running, but that's not important to the matter at hand. When you call logTrace, for example, Tcl will actually call 'log TRACE' and then append any additional arguments on to the end. So, 'logTrace foo bar' becomes 'log TRACE foo bar'.

You are concerned with porting a large body of LISP code to Tcl and want to do as few mental gymnastics as possible, which is understandable. I think it's probably safe to say in your specific case, wherever you see apply in the LISP code you can just replace it with "eval". Then take the extra step of protecting things that don't require any extra parsing.

Bryan Oakley