# dictutils.tcl -- # # Various dictionary utilities. # # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk). # # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style). # package require Tcl 8.6 package provide dictutils 0.2 namespace eval dictutils { namespace export equal apply capture witharray nlappend namespace ensemble create # dictutils witharray dictVar arrayVar script -- # # Unpacks the elements of the dictionary in dictVar into the array # variable arrayVar and then evaluates the script. If the script # completes with an ok, return or continue status, then the result is copied # back into the dictionary variable, otherwise it is discarded. A # [break] can be used to explicitly abort the transaction. # proc witharray {dictVar arrayVar script} { upvar 1 $dictVar dict $arrayVar array array set array $dict try { uplevel 1 $script } on break {} { # Discard the result } on continue result - on ok result { set dict [array get array] ;# commit changes return $result } on return {result opts} { set dict [array get array] ;# commit changes dict incr opts -level ;# remove this proc from level return -options $opts $result } # All other cases will discard the changes and propagage } # dictutils equal equalp d1 d2 -- # # Compare two dictionaries for equality. Two dictionaries are equal # if they (a) have the same keys, (b) the corresponding values for # each key in the two dictionaries are equal when compared using the # equality predicate, equalp (passed as an argument). The equality # predicate is invoked with the key and the two values from each # dictionary as arguments. # proc equal {equalp d1 d2} { if {[dict size $d1] != [dict size $d2]} { return 0 } dict for {k v} $d1 { if {![dict exists $d2 $k]} { return 0 } if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 } } return 1 } # apply dictVar lambdaExpr ?arg1 arg2 ...? -- # # A combination of *dict with* and *apply*, this procedure creates a # new procedure scope populated with the values in the dictionary # variable. It then applies the lambdaTerm (anonymous procedure) in # this new scope. If the procedure completes normally, then any # changes made to variables in the dictionary are reflected back to # the dictionary variable, otherwise they are ignored. This provides # a transaction-style semantics whereby atomic updates to a # dictionary can be performed. This procedure can also be useful for # implementing a variety of control constructs, such as mutable # closures. # proc apply {dictVar lambdaExpr args} { upvar 1 $dictVar dict set env $dict ;# copy lassign $lambdaExpr params body ns if {$ns eq ""} { set ns "::" } set body [format { upvar 1 env __env__ dict with __env__ %s } [list $body]] set lambdaExpr [list $params $body $ns] set rc [catch { ::apply $lambdaExpr {*}$args } ret opts] if {$rc == 0} { # Copy back any updates set dict $env } return -options $opts $ret } # capture ?level? ?exclude? ?include? -- # # Captures a snapshot of the current (scalar) variable bindings at # $level on the stack into a dictionary environment. This dictionary # can later be used with *dictutils apply* to partially restore the # scope, creating a first approximation of closures. The *level* # argument should be of the forms accepted by *uplevel* and # designates which level to capture. It defaults to 1 as in uplevel. # The *exclude* argument specifies an optional list of literal # variable names to avoid when performing the capture. No variables # matching any item in this list will be captured. The *include* # argument can be used to specify a list of glob patterns of # variables to capture. Only variables matching one of these # patterns are captured. The default is a single pattern "*", for # capturing all visible variables (as determined by *info vars*). # proc capture {{level 1} {exclude {}} {include {*}}} { if {[string is integer $level]} { incr level } set env [dict create] foreach pattern $include { foreach name [uplevel $level [list info vars $pattern]] { if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue } upvar $level $name value catch { dict set env $name $value } ;# no arrays } } return $env } # nlappend dictVar keyList ?value ...? # # Append zero or more elements to the list value stored in the given # dictionary at the path of keys specified in $keyList. If $keyList # specifies a non-existent path of keys, nlappend will behave as if # the path mapped to an empty list. # proc nlappend {dictvar keylist args} { upvar 1 $dictvar dict if {[info exists dict] && [dict exists $dict {*}$keylist]} { set list [dict get $dict {*}$keylist] } lappend list {*}$args dict set dict {*}$keylist $list } # invoke cmd args... -- # # Helper procedure to invoke a callback command with arguments at # the global scope. The helper ensures that proper quotation is # used. The command is expected to be a list, e.g. {string equal}. # proc invoke {cmd args} { uplevel #0 $cmd $args } }