map in TCL
Okay, this is nowhere near as cool as continuations-using-exceptions, but I figured I may as well toss this out here…
Just for grins today I implemented a functional-style map in TCL.
If you’re not familiar with TCL that might not sound like a big deal (and it’s not, really), but do note that TCL lacks:
- first-class functions
- lexical closures
Instead, the tools you’re given to work with are:
- string evaluation in arbitrary stack frames
First Try
An initial cut looks something like this:
proc map {body values} {
set result {}
foreach value $values {
uplevel 1 [ list set value $value ]
lappend result [ uplevel 1 $body ]
}
return $result
}
uplevel 1 evaluates its arguments, after parsing them into tokens and concatenating the resulting lists into a single flattened string, in the context of the caller.
The [ list set value $value ] bit constructs a TCL “list” representing code to set “value” to the given value. These lists are often, though not exactly, identical with typical TCL syntax. But note that TCL lists are still notionally represented as raw strings. For lists of trivial values, [ list 1 2 3 ], {1 2 3} and "1 2 3" have precisely the same ultimate value.
Naive TCL programmers might be tempted to simply write "set value $value" instead of [ list set value $value ], but that will fail for nontrivial values (i.e. those containing whitespace or special escape characters) because ordinary string interpolation will not introduce the necessary additional quoting.
(Also note that the scope of value in the caller will not be limited to the “block”, since TCL doesn’t have that kind of scoping.)
Anyway, usage of our custom map looks like this:
% map { expr { 1 + $value } } {1 2 3}
Which evaluates to the list:
2 3 4
Choosing a Variable Name
Of course, it would be nice to be able to choose the name of the variable used for iteration in the caller:
proc map {caller_var body values} {
set result {}
foreach value $values {
uplevel 1 [ list set $caller_var $value ]
lappend result [ uplevel 1 $body ]
}
return $result
}
Thusly:
map i { expr { 1 + $i } } {1 2 3}
n.b. expr is one of several TCL commands that applies an alternate syntax to the interpretation of its arguments—infix arithmetic (or really any arithmetic) is not part of TCL’s standard (well… common) list-based syntax.
A Handy Shortcut
Now, because the above uplevel + set pattern is so ugly, the TCL language also provides upvar, which creates a local alias for a variable in another stack frame. Here, we can use it to get rid of one of the uplevels:
proc map {caller_var body values} {
upvar 1 $caller_var var
set result {}
foreach value $values {
set var $value
lappend result [ uplevel 1 $body ]
}
return $result
}
In fact, we can just make our local value an alias for the caller’s variable directly, getting rid of the middleman:
proc map {caller_var body values} {
upvar 1 $caller_var value
set result {}
foreach value $values {
lappend result [ uplevel 1 $body ]
}
return $result
}
In case you’re wondering, despite 1 being the default stack level, I’m explicitly specifying it in upvar and uplevel to accomodate cases where either the caller variable or the body start with a digit or #. Unlikely, but legal. Considerations like this are important to writing robust code in TCL.
One Last Thing
Incidentally, if you just want to do string concatenation,
map i { "foo: $i" } {1 2 3}
won’t work as expected—TCL will try to evaluate “foo: 1” as a procedure name. Instead, you’ll need to introduce an identity function:
proc id {value} { return $value }
So you can do:
map i { id "foo: $i" } {1 2 3}
In Conclusion
I hope this article illustrates some important techniques for writing robust and generic code in the TCL language. I’ve been a TCL programmer for about a decade (three years by choice, seven years because people were paying me lots of money), and it took a long time to learn some of the lessons encapsulated above.
There’s something else I’ve realized.
TCL’s fundamental conceit that everything is simply an immutable string with arbitrary (context-dependant) syntax. It also has an artificial abstraction called “lists” which are really just whitespace-separated strings with byzantine quoting rules, upon which the language’s syntax is loosely based.
In other words:
TCL : Scheme :: Bizarro : Superman