tclsh> # squaring all elements in a list
tclsh> proc sqr {x} {expr {$x*$x}}
tclsh> ::struct::list map {1 2 3 4 5} sqr
1 4 9 16 25
tclsh> # Retrieving the second column from a matrix
tclsh> # given as list of lists.
tclsh> proc projection {n list} {::lindex $list $n}
tclsh> ::struct::list map {{a b c} {1 2 3} {d f g}} {projection 1}
b 2 fIn tcl 8.5 they can be rewritten as one liners like this:
# squaring
% ::struct::list map {1 2 3 4 5} {apply {x {expr {$x*$x}}}}
1 4 9 16 25
# projection
% ::struct::list map {{a b c} {1 2 3} {d f g}} {apply {x {lindex $x 1}}}
b 2 fComments and improvements to this form can be reviewed in foreach little friends, where I talked about struct::list and apply as an introduction to something else.This is only a step towards list comprehensions. I used list comprehensions a lot in Python and I miss them in tcl. List comprehensions have several advantages over other methods of forming lists:- A one liner, yet very clear
- Can handle more than one list
- Engages both mapping and filtering
# squaring >>> lis = [1,2,3,4,5] >>> lis = [x*x for x in lis] >>> lis [1, 4, 9, 16, 25]Given lis = [1,2,3,4,5] one can calculate the sum of squares in a one short clear line
>>> sum([x*x for x in lis]) 55And here is the projection example
# projection >>> [x[1] for x in [['a','b','c'],[1,2,3],['d','f','g']]] ['b', 2, 'f']Working with more than one list using zip:
# calculating the differences between elements from two lists and taking only the ones that are not 0 >>> list1 = [10,20,30,40,50] >>> list2 = [9, 20, 28, 40, 50] >>> print [x-y for x,y in zip(list1, list2) if x-y > 0] [1, 2]A few times I needed to count all the non empty lines in a file. A brute force, yet elegant list comprehension solution is
# txt holds the entire text
lis = [line for line in txt.splitlines() if line.strip() != ""]
coutn = len(lis)
coutn = len(lis)Indeed a regular expression will do here too, but crafting it will not take 20 (or less) seconds - the time needed to write the list comprehension.The same thing with tcl would probably be set lines {}
foreach line [split $txt \n] {
if {[string trim $line] ne ""} {lappend lines $line}
}
set count [split $lines \n]or regsub -all -lineanchor {^\s*?\n} $txt "" txt2
set count [split $txt2 \n]
set count [split $txt2 \n]none of which, I feel, fits the task properly. The first one is too long for such a simple thing, and the second one, well, didn't come out right on the first trial.Python (like Haskell) also supports nested fors like in>>> lis1 = [1,2,3] >>> lis2 = ['a','b','c'] >>> [[x,y] for x in lis1 for y in lis2] [[1, 'a'], [1, 'b'], [1, 'c'], [2, 'a'], [2, 'b'], [2, 'c'], [3, 'a'], [3, 'b'], [3, 'c']]but I'll do without it for now.Is this nice syntax can be imported to tcl? I would like it in further versions. I played with several forms of list comprehensions. The amount of braces stemming from tcl's syntax realy makes it difficult to come up with a nice syntax. Eventually, inspired by bind and little language I've come up with this:
# A helper proc: perform foreach the paramters given as one list
proc foreachlist {list body} {
uplevel 1 foreach $list [list $body]
}
# list comprehension
proc lisco {group {var ""}} {
# extract params
regexp {(.*?)(?:\sfor\s)(.*?)(?:\sif\s(.*?)$|$)} $group dummy cmd lists if
if {$if eq ""} {set if 1}
# generate foreach line and string-map expression
set nums 0
set mapExp {%% % }
foreach list [uplevel 1 [list subst [uplevel 1 list $lists]]] { ;# 8-(
incr nums
lappend foreachLine $nums $list
lappend mapExp %$nums $$nums
}
# build result list
set res {}
foreachlist $foreachLine {
set mapExp2 [subst $mapExp]
set cmd2 [string map $mapExp2 $cmd]
set rtmp [uplevel 1 $cmd2]
set cond [string map [concat $mapExp2 [list %r $rtmp]] $if]
if {[uplevel 1 [list expr $cond]]} {lappend res $rtmp}
}
if {$var ne ""} {uplevel 1 [list set $var $res]}
return $res
}Here are a few examples using it:
# squaring
% lisco {expr {%1*%1} for {1 2 3 4 5}}
1 4 9 16 25Why waste space on named arguments? One list requires one argument - %1.Now with two lists: two lists:
set list1 {10 20 30 40 50}
set list2 {9 20 28 40 50}
lisco {expr {%1-%2} for $list1 $list2 if [expr %1-%2] > 0} result
puts $result
1 2Another convenience 'keyword' is %r (for 'reference'), with which the previous lisco may be written like this:
lisco {expr {%1-%2} for $list1 $list2 if %r > 0} result
puts $result
1 2Better the Python!The auto-variables, %1, %2, %r are substituted using string map so when they might contain lists, they need to be grouped. Like in the projection example:
# projection
% lisco {lindex "%1" 1 for {{a b c} {1 2 3} {d f g}}}
b 2 fNow counting the non-empty lines may be achieved this way lisco {list %1 for [split $txt \n] if [llength "%r"] > 0} lines
or
lisco {format "%1" for [split $txt \n] if [string trim "%r"] ne ""} lines
then
set counlines [llength $lines]lisco might need more polishing. It also supports only %1-%9 variables (who needs more anyway). The for and if words are used as keywords so you can't have them anywhere else in the expression (not so good if you've got a list containing one of them...), but the idea is clear: Having a nice syntax for forming lists, which will be convenient to use. Like in Python.RS For the line counting, the regexps need not be very complicated. My train of thought was: count newlines (\n), subtract double-newlines (\n\n):
iu2 I've tried it on
NEM: List comprehensions can be generalised to support not just lists, but any structure supporting two operations: one which wraps a value into an instance of the structure, and another which takes a function and applies it to a member of the structure, returning a new instance of the structure. Structures which provide these two operations are known as monads. See that page for a generalised list comprehension syntax. For instance, here are some of your Python examples using the list monad:
iu2: Zipping usually works with two or more arguments:
See also List Comprehension, lcomp
expr {[regexp -all \n $txt] - [regexp -all \n\n $txt]}But of course this is no list comprehension, just a "string comprehension" alternative :^)iu2 I've tried it on
set txt {
hello,
This is a
line of
text and another
line
}There are 5 non-empty lines and using expr {[regexp -all \n $txt] - [regexp -all \n\n $txt]}gives 10 Or did you mean counting all the lines?slebetman: It depends on what you mean by "empty" lines. RS's code, while straightforward, considers lines containing whitespase to be non-empty. This is obviously not what you expected. Would this work?:
expr {[regexp -all \n $txt] - [regexp -all \n\s*?\n $txt] - [regexp ^\s*?\n $txt]}Note that the last regexp is needed to handle when the first line is empty like your example $txt. Again, this "bug" clearly demonstrates that using regexp for this is much less intuitive.iu2: Yes it worked. And this works too regexp -all -lineanchor {^\s*\S+[^\n]+\n} $txtbut again, it take more time to craft. regexp requires crafting while list comprehensions don't.NEM: List comprehensions can be generalised to support not just lists, but any structure supporting two operations: one which wraps a value into an instance of the structure, and another which takes a function and applies it to a member of the structure, returning a new instance of the structure. Structures which provide these two operations are known as monads. See that page for a generalised list comprehension syntax. For instance, here are some of your Python examples using the list monad:
# specialise the monad do-notation for lists
interp alias {} lcomp {} do List
set lis {1 2 3 4 5}
# square each member of the list
lcomp x <- $lis { yield [expr {$x*$x}] }
# projection
lcomp x <- {{a b c} {1 2 3} {d f g}} { yield [lindex $x 1] }
# multiple lists using zip
proc zip {xs ys} {
set ret [list]
foreach x $xs y $ys { lappend ret [list $x $y] }
return $ret
}
set list1 {10 20 30 40 50}
set list2 { 9 20 28 40 50}
lcomp {x y} <- [zip $list1 $list2] { if {$x-$y>0} { yield [expr {$x-$y}] } else fail }
# Multiple inputs - produces all combinations of ($x,$y)
lcomp x <- {a b c} y <- {1 2 3} { yield ($x,$y) }
# Filter text for non-blank lines
lcomp line <- [split $txt \n] { if {[string trim $line] ne ""} { yield $line } else fail }It's performance is decent too, given the flexibility.iu2: Zipping usually works with two or more arguments:
proc zip {args} {
set res {}
for {set c 0} {$c < [llength [lindex $args 0]]} {incr c} {
set tmp {}
foreach list $args {
lappend tmp [lindex $list $c]
}
lappend res $tmp
}
return $res
}
set list1 {1 2 3 4 5 6}
set list2 {a b c d e}
set list3 {10 20 30 40 50}
puts [zip $list1 $list2 $list3 {One Two Three Four Five Six}]
Result
{1 a 10 One} {2 b 20 Two} {3 c 30 Three} {4 d 40 Four} {5 e 50 Five} {6 {} {} Six}See also List Comprehension, lcomp

