Version 11 of Procedures stored in arrays

Updated 2002-11-22 10:55:26

William Wragg 21-nov-2002 - 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.

(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 "{seed [incr seed]}" [info body $self]
    set seed
  }

  # Lambda proc with caller scope life dependenecy.
  # 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 variable __$name u {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
    1409286144

Another simple example would be:

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

  call $arr(hello) "hello world"
    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]
    pfac:81 microseconds per iteration
  puts spfac:[time {call $s(spfac) 30} 1]
    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)

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] != "sproc"} {
      error "Not a stored procedure: should be \"{sproc args body}\""
    } else {
      set arguments [lindex $sproc 1]
      set code [lindex $sproc 2]
      set len_arguments [llength $arguments]
      set len_args [llength $args]
      set llen_arguments [expr $len_arguments - 1]
      set llen_args [expr $len_args - 1]

      # Set the variables to the given args after checking validity.
      if {$len_args < $len_arguments} {
        error "wrong # args: should be \"$arguments\""
      } elseif {($len_args > $len_arguments) && [lindex $arguments $len_arguments] != "args"} {
        error "wrong # args: should be \"$arguments\""
      } else {
        for {set i 0} {$i < $llen_arguments} {incr i} {
          set [lindex $arguments $i] [lindex $args $i]
        }

        if {[lindex $arguments $llen_arguments] == "args"} {
          set args [lreplace $args 0 [expr $llen_arguments - 1]]
        } else {
          set [lindex $arguments $llen_arguments] [lindex $args $llen_arguments]
        }

        # Run the code.
        eval $code
      }
    }
  }

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} 1]
    spfac:1358 microseconds per iteration

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 variable s(hello) r {puts "hello called" ;#}
  call $s(hello) "hello there"
    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}}

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