You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
6.1 KiB
143 lines
6.1 KiB
# 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 } |
|
|
|
}
|
|
|