Procedures stored in arrays

William Wragg 2002-11-21: While playing with Lua I discovered the joys of first class functions. This combined with the use of associative arrays was so much fun to program with that I started wanting them in other languages I use. I found a similar idea in Arrays as cached functions but this wasn't quite what I wanted. What I wanted were for procs to be stored in arrays, and then called by just getting the value of the array.

Larry Smith You might be interested in MOST.

(Also see Arrays of function pointers - KPV)

This is what I came up with:

# unique integer ID generator, at first call gives 1, then 2, 3, ...
proc intgen {{seed 0}} {
    set self [lindex [info level 0] 0]
    proc $self [list [list seed [incr seed]]] [info body $self]
    return $seed
}

# Lambda proc with caller scope life dependency.
# When the caller scope dies so does the lambda proc.
proc sproc {args body} {
    set name sproc[intgen]
    uplevel 1 set __$name -
    uplevel 1 "trace add variable __$name unset {rename $name {} ;#}"
    proc $name $args $body
    return $name
}

# Use this proc to run the sprocs stored in arrays.
# The value from the array - sproc - is eval'ed to produce
# the proc and name. This proc name and the args list is
# then eval'ed again to produce the output.
proc call {sproc args} {
    eval [eval $sproc] $args
}

The sproc procedure is just a lambda procedure from Lambda in Tcl. To use the above do something like - note the use of the global namespace for accessing the array value:

set s(spfac) {
    sproc x {
        expr {$x<2? 1: $x * [call $::s(spfac) [incr x -1]]}
    }
}

Which is the same as:

proc pfac x {
    expr {$x<2? 1: $x * [pfac [incr x -1]]}
}

You use the sproc like:

call $s(spfac) 30

Output:

1409286144

Another simple example would be:

set arr(hello) {
    sproc var {
        puts $var
    }
}
call $arr(hello) {hello world}

Output:

hello world

The overall affect is almost what I wanted. The way of calling the sprocs is a bit gludgy, but all other ways I could think of used traces, which I didn't want. I wanted everything to be in the array and a few handler procs. Creating and destroying procs is not the fastest process in the world but there are other ways - see below:

puts pfac:[time {pfac 30} 1]

Output:

pfac:81 microseconds per iteration
puts spfac:[time {call $s(spfac) 30} 1]

Output:

spfac:2749 microseconds per iteration

But hell it was fun figuring it all out. These could even be combined with Persistent arrays or Tequila for something interesting.


KPV: My biggest problem with ideas like this and also things like Arrays of function pointers and even object inheritance is that it can make it very hard for someone new to a code base to read code and understand what's going on. Trying to determine which actual procedure gets call at a given line of code can be almost impossible to determine without actually stepping the code in a debugger.

Also, how do you debug a procedure stored in an array? RS: The procs themselves are not in the array - that contains only the generated names. Having that, you can introspect e.g. with

info body $s(spfac)

WW: Actually the arrays don't store the names but the code to generate the procedure, so the above would not work. The procedure names, and the procedures, are only stored as long as the call proc is active. The above could be got with:

[lindex $s(spfac) 2]

RS: If I read your code right, call creates a new lambda every time, with constant body. I think it's much easier to do it like this:

set s(spfac) [sproc x {...}] ;# define only once
$s(spfac) 30                 ;# call by dereferencing the array element

..and for easier reading I would still use lambda for sproc, 'cause that's what it's called in the literature too...


WW: The above is not really what I wanted. I wanted to store the procedure in an array, so that if the array was modified, the procedure when run again would use the updated code, and I wanted this without traces present, unless this could be combined some how so that it was all automatic. I also wanted to be able to use the standard array procs for copying, changing etc. the sprocs.

I called the lambda part of the helper procs sproc as I was going to add some additional stuff which moved sproc away from a lambda proc, infact away from a proc all together. The stuff below is what I was going to add. Your all too fast for me :o)

proc call {sproc args} {
    if {[llength $sproc] != 3} {
        error "wrong # args: should be \"sproc args body\""
    } elseif {[lindex $sproc 0] ne {sproc}} {
        error "Not a stored procedure: should be \"{sproc args body}\""
    } else {
        set locals [lindex $sproc 1]
        set body [lindex $sproc 2]
        set vals $args
        set len_locals [llength $locals]
        set len_vals [llength $vals]
        
        # Set the variables to the given args after checking validity.
        if {$len_vals < $len_locals} {
          error "wrong # args: should be \"$locals\""
        } elseif {($len_vals > $len_locals) && ([lindex $locals end] != "args")} {
          error "wrong # args: should be \"$locals\""
        } else {
          # Create all the variables and assign the arguments passed in.
          foreach $locals $vals {}
          
          # Set "args" if it exists.
          if {[lindex $locals end] eq {args}} {
            set args [lrange $vals [expr $len_locals - 1] end]
          }
        
          # Run the body.
          eval $body
        }
    }
}

The above proc replaces the three (sproc, intgen, and call) original helper procedures with just the one call proc. The sproc is kept as a flag, so that arrays which store procedures and data together can differentiate easily between them. The single call proc is also faster:

puts spfac:[time {call $s(spfac) 30} 10]

output:

spfac:803 microseconds per iteration

The only thing that stored procedures don't implement is default values, but this could easily be added to the above call proc.

The stored procedures can be traced like any other variable to show when one has been called or altered etc...:

set s(hello) {sproc {var} {puts $var}}
trace add variable s(hello) read {puts "hello called" ;#}
call $s(hello) "hello there"

Output:

hello called
hello there

Built in procs can be wrapped in an sproc so that changes can be made to the sproc without affecting the builtin one:

set builtIn(puts) {sproc {args} {eval puts $args}}

Lambda procs can be simulated with:

call {sproc args body} args

Well I think that wraps it up. I might try a small fully distributed and persistent app. with these at some point, just for fun. :o)


ulis, 2005-08-07. If you want to have an array of procs, the simplest way is:

for {set i 0} {$i < 10} {incr i} {
  proc p($i) {} [list puts "proc p($i)"]
}
for {set i 0} {$i < 10} {incr i} {
  p($i)
}

AMG: Clever. This isn't really an array, but it looks and behaves much like one. It's not even a variable.


DKF: Now that Tcl 8.5 is out, consider using apply's efficient lambda terms instead.


AMG: See also sproc.