https://world-playground-deceit.net/blog/2024/10/tcl-closures.html World Playground Deceit.net [*]Blog New Posts [ ]2025 [ ]05 [ ]Novel review: S.M. Stirling - Under the Yoke (1989) The novel The ideas [ ]04 Music review: Deathspell Omega - Si Monvmentvm Reqvires, Circvmspice (2004) [ ]Ogg Vorbis cover art embedding: Tcl vs Common Lisp In Tcl In Common Lisp In MY Common Lisp [ ]Adding keyword parameters to Tcl procs How it's made Movie review: Ford v Ferrari (2019) Music review: Dead Can Dance - Within the Realm of a Dying Sun (1987) Music review: Dark Tranquility - Skydancer (1993) [ ]03 Music review: Darkthrone - Goatlord (1996) Music review: Danzig - Danzig II - Lucifuge (1990) Music review: Crowbar - Crowbar (1993) Speeding up cljqalbum Pruning Portage package config files Music review: Comus - First Utterance (1971) Movie review: Idiocracy (2006) A Common Lisp jq replacement Music review: Cocteau Twins - Heaven or Las Vegas (1990) [ ]02 Music review: Blut Aus Nord - Ultima Thulee (1995) [ ]Music questions challenge What are five of your favorite albums? What are five of your favorite songs? Favorite instrument(s)? What song or album are you currently listening to? Do you listen to the radio? If so, how often? How often do you listen to music? How often do you discover music? And how do you discover music? What's a song or album that you enjoy that you wish had more recognition? What's your favorite song of all time? Has your taste in music evolved over the years? Music review: Zhui Ming Lin Qin (Shiina Ringo) - Sheng Su sutoritsupu (Shoso Strip) (2000) Linux pipe(2) vs ring buffer Novel review: Robert Heinlein - The Moon is a Harsh Mistress (1966) Music review: Black Flag - My War (1984) [ ]01 Music review: Belketre - Ambre Zuerkl Vuorhdrevarvtre (1996) Cooking recipe: boeuf bourguignon Most memorable extreme music screaming Music review: Amesoeurs - Ruines Humaines (2006) Novel review: Terry Pratchett - Mort (1987) Novel review: Mark Danielewski - House of Leaves (2000) PDF to CBZ for e-reader Music review: Acid Bath - Paegan Terrorism Tactics (1996) Video game review: Bloodstained: Ritual of the Night (2019) Dotfiles management [*]2024 [ ]12 Novel Review: Liu Cixin - The Three-Body Problem (2008) GNU parallel out, pararun in Advent of Code 2024: retrospective Advent of Code 2024: Day 01 [ ]11 [ ]How I Learned to Stop Worrying and Love GC Initial C weenie stance Insights and updated views New stance See also Music review: Thee Maldoror Kollective - New Era Viral Order (Dogma Slaughterhouse and the Children of Anaemia) (2002) Novel review: Terry Pratchett - Guards! Guards! (1989) & Men at Arms (1993) [ ]Smuggling files under Google's nose First blood: nested zips Second skirmish: file signature doctoring Final setback and victory My archives Emacs: separate minibuffer history for a command Music review: Techno Animal - The Brotherhood of the Bomb (2001) CL iterate's COLLECT performance notice [*]10 Music review: Burzum - Det som engang var (1993) QoL addition to Common Lisp :start/:end [*]Closures in Tcl What kind of closures In Tcl Music review: Abigor - Supreme Immortal Art (1998) git secret filter [ ]My sfeed setup Introduction Sfeed praise Personal additions [ ]Why I like Tcl Pros Cons Conclusion [ ]09 Music review: Samael - Eternal (1999) Movie review: The Crow (1994) [ ]Bourne shell stdio trivia while read loops and interactive commands Default stream buffering [ ]Tags fantasy (1) advent of code (2) alternate history (1) black metal (6) blackgaze (1) comedy (3) computer science (1) cooking recipe (1) darkwave (1) dotfiles (5) emacs (1) fantasy (1) folk (1) gentoo (1) git (1) gothic metal (1) hacking (1) heavy metal (1) hip hop (1) horror (1) image processing (1) industrial metal (1) j-rock (1) lisp (5) melodic death metal (1) movie review (3) music review (19) music (1) novel review (6) performance (3) personal (2) post-punk (1) programming (15) punk (1) science fiction (2) sh (4) sludge metal (1) stoner metal (1) tcl (4) video game review (1) [ ]About Me [ ]Website Raison d'etre Design choices [ ]Links [ ]Webrings Lainring Code Closures in Tcl Published on 2024-10-19 Tags: tcl, programming --------------------------------------------------------------------- While closely following the discussions spawned from the recent Tcl/ Tk 9.0 release, I've noticed a point that keeps coming up: the absence of closures. Usually the cue for every Tcl hacker in the world (a very large mob, let me tell you) to start showcasing various contraptions to emulate them. So here's my turn. What kind of closures SS But first, let me explain that what I think of when I read the word "closure". You see, most C++ers would say that this is a closure: #include auto make_counter(int x = 0) { return [x]() mutable {return ++x;}; } int main(void) { auto counter = make_counter(); printf("counter: %d\n", counter()); // => counter: 1 printf("counter: %d\n", counter()); // => counter: 2 } But the environment isn't closed over here, it's simply copied and this copy is then allowed to be mutated. You could capture x by reference, but then it'd become a dangling reference outside its scope (thus lifetime, for stack variables)... Let's see how Python works in comparison: def make_counter(x=0): def counter(): nonlocal x x += 1 return x counter() print(f'x: {x}') # => x: 1 return counter counter = make_counter() print(f'counter: {counter()}') # => counter: 2 print(f'counter: {counter()}') # => counter: 3 As you can see here, the closed over variable is truly captured, not just its value, but the closure stays valid outside its scope. In C++, this could be achieved if all local variables were in fact std::shared_ptr captured by value. You might wonder why you'd ever need such a strange behaviour, right? Well, I've encountered this use case a few times in Lisp: (defun tree-walk (tree callback) ...) (defun find-integer-nodes (tree) (let ((result)) (tree-walk tree (lambda (node) (if (integerp node) (push node result)))) result)) where a callback is used to collect various items. Again, this specific case would work using capture-by-reference in C++, but it's nice to know these closures also work outside their scope because variable lifetime is tied to their binding. If you have some mental energy to spare, I strongly recommend this fantastic article about the nitty-gritty of closures (actually, binding scope and lifetime) in ANSI Common Lisp to better understand the subtleties at play. In Tcl SS Tcl being a very small language, it doesn't have lambdas or closures builtin, but we did get apply with 8.5! A dead simple wrapper later, and we have our lambdas and even partial application as a one-liner bonus: proc lambda {args body {ns ""}} { if {$ns eq ""} { set ns [uplevel 1 namespace current] } list apply [list $args $body $ns] } set l [lambda args {puts $args}] {*}$l c "d e"; # => "c {d e}" # Partial application of {*} style callables proc papply {callable args} { concat $callable $args } But for closures, the task does seem a little harder... because while values are reference counted, variable bindings disappear once their stack frame is destroyed. The only way to keep those variables alive is by storing them in a namespace or maybe in the top-level stack frame. Since TclOO (the builtin object system) is a clean way to instantiate uniquely named namespaces, that's what I went with: namespace eval closure { proc new {vars args body {ns ""}} { if {$ns eq ""} { set ns [uplevel 1 namespace current] } list [closure_class new $vars $args $body $ns] apply } # Simply forward to closure_class proc destroy {closure} {[lindex $closure 0] destroy} proc lexenv {closure args} {[lindex $closure 0] lexenv {*}$args} oo::class create closure_class { constructor {vars args body ns} { variable fun [list $args [string map [list @ [list $body]] { upvar 1 lexenv lexenv dict with lexenv @ }] $ns] variable lexenv [dict create] foreach var $vars { if {[llength $var] == 2} { dict set lexenv {*}$var } else { dict set lexenv $var [uplevel 2 set $var] } } } method apply args { my variable fun lexenv apply $fun {*}$args } method lexenv {{var ""}} { my variable lexenv if {$var ne ""} { dict get $lexenv $var } else { set lexenv } } } namespace export new lexenv destroy namespace ensemble create } set i 0 set counter [closure new {i} {} {incr i}] # Same as [closure new {{i 0}} {} {incr i}] puts "counter: [{*}$counter]"; # => counter: 1 puts "counter: [{*}$counter]"; # => counter: 2 puts "lexenv: [closure lexenv $counter]"; # => lexenv: i 2 puts "lexenv: [closure lexenv $counter i]"; # => lexenv: 2 closure destroy $counter; # Needed to avoid leaks In the end, I had the same limitations as the C++ version (environment being copied), but being able to access the stored environment via that lexenv method does make the aforementioned gathering trick possible, even if a bit different in appearance. The destroy method call being necessary until TIP 550 is implemented is a bit of a pain, but that's how it is. In the craziest parts of my mind, I did imagine an environment writeback after each apply method call together with a way to disable that once leaving the stack frame where the closure was created (via an uplevel'd defer) but I'll let the idea sit for a while. Made with Emacs, LISP and spinneret, served by CaddyGenerated by make-website Sat, 03 May 2025 20:19:44 +0000