Deferred evaluation

Roy Keene I have submitted a "defer" module to Tcllib to bring Go-like defer to Tcl.

Richard Suchenwirth 2002-04-28 - Here is an attempt to model deferred evaluation with traces: three lines of code in willset (the will indicating future tense, of course ;-) exercise all subcommands of the trace command. First a previous trace is checked and possibly deleted, then the new one registered.

The idea is that a variable is "bound to" a "body" which is executed when the variable value is retrieved, so references to other variables are re-evaluated, and changes to them reflected accordingly. Example:

set a {1 2}
set b {3 4}
willset c {list $a $b} => {1 2} {3 4}
set b {30 4}
set c => {1 2} {30 4}

proc willset {varName body} {
        set trace [uplevel 1 trace info variable $varName] ;# have previous?
        if {$trace != ""} {uplevel 1 eval trace remove variable $varName $trace}
        uplevel 1 [list trace add variable $varName read "set $varName \[$body\];#"]
        uplevel 1 [list set $varName]
}

Roy Keene Similar to "willset" above, but with only one round of replacement, here is a procedure that will replace a variable with code later on, but only once

## Lazily evaluate a variable once
proc lazyEvalOnce {varName commandPrefix} {
        set trace [uplevel 1 trace info variable $varName]
        if {$trace ne ""} {
                uplevel 1 [list trace remove variable $varName $trace]
        }

        set isArray false
        if {[uplevel 1 [list array exists $varName]]} {
                set isArray true
        }

        set code [list apply {{isArray code name1 name2 args} {
                if {$name2 ne "" || $isArray} {
                        set varname "$name1\($name2\)"
                } else {
                        set varname $name1
                }

                if {$isArray} {
                        if {[uplevel 1 [list info exists $varname]]} {
                                return
                        }
                } else {
                        uplevel 1 [list unset -nocomplain $varname]
                }

                append code " " [list $name1 $name2 {*}$args]
                set result [uplevel 1 $code]

                uplevel 1 [list set $varname $result]
        }} $isArray $commandPrefix]

        if {$isArray} {
                uplevel 1 [list unset -nocomplain $varName]
                uplevel 1 [list array set $varName [list]]
        } else {
                uplevel 1 [list set $varName ""]
        }

        uplevel 1 [list trace add variable $varName read $code]

        return
}

Usage:

    lazyEvalOnce test {apply {args { puts "Fetching remote data"; package require http; set token [http::geturl http://google.com/]; set retval [http::ncode $token]; http::cleanup $token; return $retval }}}

As shown above, deferred assignment with willset easily passes the test given by davidw in linked lists. His added requirement for garbage collection does not seem to be an issue - the trace to an willset variable is automatically deleted when the variable ceases to exist.

As shown by Arjen Markus before, linked lists and whatever references may be good for can be implemented in Tcl. It's just that Tcl references are not "physical" pointers, but (as usual) strings: variable names, which are mapped to physical pointers in an internal hash table. The problem of "dangling references" (stored references to objects that do no more exist) is likewise there, but if it occurs, it is rather clearly reported and does not lead to a segmentation fault:

 % set a [list 1 2]
 % set b [list 3 4]
 % willset c {list $a $b}
 {1 2} {3 4}
 % set b [list 30 4]
 30 4
 % set c
 {1 2} {30 4}
 % unset a
 % set c
 can't read "c": can't read "a": no such variable

Introspection was not explicitly coded: find out what body is tied to a variable with

 trace info variable c

Mixing eager assignment (with set) and lazy assignment (with willset) currently has the troublesome property that the set returns the new value, but later accesses still fire the trace:

 % set c hello => hello
 % set c   0> {1 2} {30 4}

Obviously, this isn't a finished solution yet... the following write trace didn't work out:

        #set writetrace "trace vdelete $varName \[trace vinfo $varName\];#"
        #uplevel 1 trace var $varName w $writetrace

Maybe raising an error is enough for the write trace. Come to think, willset suddenly resembles dynavar from Braintwisters:

 willset now {clock format [clock seconds] -format %H:%M:%S}
 0 % set now
 19:37:12
 0 % set now
 19:37:14

Salvatore Sanfilippo - Deferred evaluation, and the above example on lists, for what I can see can't allow you to walk the 'c' list, modify the (say) second element, and see the changes in 'a'. This is a pretty obvious task you can do with real references. Maybe you can manage to have this form of deferred evaluation to work with my counter-example, but it will be easy to find some case when you can't do with it what you can do with references, just because they are a different stuff. Note that I didn't tried it myself, but it seems pretty evident from the code, sorry if I just misreaded your code.

Apart from this, references aren't about variables of course, they are about data that reference some other piece of data... so with deferred evaluation you can't build a simple data structure like a tree and walk inside it, modify values, and so on with the 'classical' complexity you found in every CS book. This is enough to make deferred evaluation non-equivalent (but of course an interesting piece of code that shows some kind of lazy-evaluation).


RS Yes. Even if evaluation is deferred into the future, it still follows the Tcl rule of substitution - you can read a value with $value, but if you modify it, a copy will be made, and the original value is unchanged - read-only references, "copy-on-write", somehow like (yet very different from) *const in C. For mutable variables, use the upvar mechanism and call-by-name. But I have yet to be convinced that mutation on values has advantages…


Various other probes into deferred evaluation:

  • Streams - generator-like procs with state
  • forall - custom control structurs for logical quantifiers

wusspuss - 2022-03-21 10:08:10

Here's a shorter implementation

proc defer {args} {
    if {[llength $args]==1} {set args [lindex $args 0]} ;# flatten if 1 elem 
    uplevel {set _defer_var {}}
    uplevel [list trace add variable _defer_var unset [list apply [list args $args]]]
}

Unlike defer::defer from tcllib it also allows you to pass scripts in {} Which means smth like

proc test {} {
    ...
    defer {
        while 1 {
            evaluate a complex script
        }
    }
}

will work with this defer but not defer::defer from tcllib.