[ home ]

"Do-with-list-member" ("lwith") in Tcl

(I guess that for a more seasoned Tcl-user, this would be a oneliner...)

Background and rationale

Tcl offers 'foreach' and 'dict with' constructs, allowing a given script to operate on all list items, respectively a single dict-(sub)value.

However, there is no construct to execute a script operating on a single list-item, to avoid read-modify-write on a 'list-and-index' tuple. This makes working with list-of-list or list-of-dict cumbersome.


The following code implements a simple 'lwith', taking a list-and-index and a script to be applied to the corresponding list-item. The script is executed in the caller's context, with the list-item available as 'objvar'.

If and only if the script executes succesfully (i.e. returns 'TCL___OK'), the possibly modified list-item is re-inserted into the list.

    proc lwith { objvar listvar index script } {
        upvar $objvar  obj
        upvar $listvar li
        set obj [ lindex $li $index ]
        try        { uplevel $script 
        } on ok {} { lset li $index $obj }


As an example, a list of 'book'-objects is modified in-place.


Here's a simple dict-based 'book'-class to play around with:

    proc book.new { title author pagecount } { 
        dict create  title $title  author $author  pagecount $pagecount 
    proc book.summary { book } { 
        format "'%s'  (%s), %d pages"      \
            [ dict get $book title     ]   \
            [ dict get $book author    ]   \
            [ dict get $book pagecount ]

Using "lwith" on a an item in a list of books:

First, a list of books is created ...

    set books [ list                                                           \
        [ book.new  "My First Ponybook"               Ponyboy          10 ]    \
        [ book.new  "Illustrated Pony Encyclopaedia"  "Dr. Ponylove"  614 ] ]
    foreach book $books { puts [ book.summary $book ] }

... with output as follows:

    'My First Ponybook'  (Ponyboy), 10 pages
    'Illustrated Pony Encyclopaedia'  (Dr. Ponylove), 614 pages

Then, the 1st list-item is modified in-place using 'lwith' ...

    lwith book books 0 {
        dict incr   book pagecount 
        dict append book author " the Third"
    foreach book $books { puts [ book.summary $book ] }

... with the following output:

    'My First Ponybook'  (Ponyboy the Third), 11 pages
    'Illustrated Pony Encyclopaedia'  (Dr. Ponylove), 614 pages

A note on readability vs 1337ness

Warning: this is probably a personal issue.

Recently I more and more prefer code that is easy to read, versus things like brevity, pre-mature optimisation, and 1337ness.

I find myself spending quite a bit of time re-typing bits of code to find out which form seems easiest to digest.

(Most code is read many, many times more than it is written. Therefore, let's not anger our future colleagues. :-)

An example of a shorter version of 'lwith':

    proc lwith_shorter { objvar listvar index script } {
        uplevel set $objvar \[ lindex \[ set $listvar ] $index ]
        try        { uplevel $script 
        } on ok {} { uplevel lset $listvar $index \[ set $objvar ] }