Memoizing wrapper for Tcl procs
I made a small fix to my MangaDex downloader this afternoon which in turn made me fiddle until I got a neat and tidy proc memoization wrapper as commonly available in dynamic languages. And as a bonus, a similar
wrapper to get rate limiting for my API calls!
proc proc_memo {name args body} { set cvar [string cat $name _memo_cache] global $cvar set $cvar [dict create] tailcall proc $name args [quasiquote { global `$cvar if {[dict exists $`$cvar $args]} { return [dict get $`$cvar $args] } set ret [apply {`$args `$body `[uplevel 1 namespace current]`} {*}$args] dict set `$cvar $args $ret return $ret }] } proc_memo p {x y} { puts "args: $x $y" string cat $y $x } puts [p 1 2]; # => args: 1 2↵21 puts [p 1 2]; # => 21 puts [p 3 4]; # => args: 3 4↵43 puts [p 3 4]; # => 43
It Just Werks™, in large part thanks to my precious quasiquote helper. Really too bad global doesn't allow for clean "once only" initialization like C's static or CL's defvar…
An LRU evicting version would be necessary for real-world use - to avoid unbounded cache growth - but here Tcl showcases the limitations RMS infamously ranted about more than 30 years ago: you can't build complex datastructures from scratch (i.e. not based on the builtin ones) like an LRU cache that needs at least a linked list with head/tail access, no if no but.
One would have to either cobble something with struct::record or more realistically, painfully write it in C via Critcl to get the required high performance.
Well, let's put ranting aside and take a look at the rate-limiting version:
proc proc_rate_limited {name callnum/duration args body} { set fvar [string cat $name _rate_limit_fifo] global $fvar set $fvar [list] lassign [split ${callnum/duration} /] callnum duration set duration_ms [uscale $duration ms] tailcall proc $name args [quasiquote { global `$fvar set now [clock milliseconds] if {[llength $`$fvar] == `$callnum} { set `$fvar [lassign $`$fvar last] if {($now - $last) < `$duration_ms} { after [expr {round(`$duration_ms - ($now - $last))}] } } lappend `$fvar $now apply {`$args `$body `[uplevel 1 namespace current]`} {*}$args }] } # No more throttling from MangaDex! # cf https://api.mangadex.org/docs/2-limitations/#general-rate-limit proc_rate_limited api_get 5/1.1s {endpoint {query_params ""}} { exec curl ... }
Clean, isn't it? Well, I do think so, at least.