Author: Donal K. Fellows <dkf@users.sf.net>
State: Draft
Type: Project
Tcl-Version: 9.1
Vote: Pending
Created: 31-Oct-2025
Keywords: Tcl
Tcl-Branch: tip-735
Abstract
Sometimes, it's useful to pick out a selection of elements from a list according to some boolean predicate. This TIP makes that more convenient.
Rationale and Example
It's not exactly unheard of for code to want to pick items out of a list. Tcl does it inside the ICU interface with:
set unmappedNames {}
foreach tclName [encoding names] {
# Note entry will always exist. Check if empty
if {[llength [tclToIcu $tclName]] == 0} {
lappend unmappedNames $tclName
}
}
In other cases, lsearch -all -inline is used for that, where the condition
can be expressed as a pattern match.
However, that's not very obvious. It would be significantly more obvious if we could instead do:
set unmappedNames [lfilter tclName [encoding names] {
# Note entry will always exist. Check if empty
[tclToIcu $tclName] eq ""
}]
So let's make that work! Note that the body is an expression that produces a value interpreted as a boolean, and that we select the values that can be interpreted as true (and reject the ones that are false). Note in the specific example that Tcl 9.1 optimizes the comparison with an empty string case so that it's efficient for testing for empty lists among other things. This is a consequence of TIPs 711 and 720.
The name and functionality are inspired partially by the general filter()
predicate in some functional programming languages, given that map()
corresponds to lmap.
Specification
Let there be a command, lfilter, with the signature:
lfilter varList0 list0 ?varList list...? bodyExpression
The pattern of varList/list items is as for foreach and lmap.
The command will iterate over the items of the list arguments in order,
binding the variables from the varLists. For each of those steps, the
bodyExpression will be evaluated (as if with expr) and the result will
be interpreted as a boolean; if it is interpretable as a true value (according
to Tcl_GetBoolean) then the value that the the first variable of varList0
was set to (though not the value that it necessarily now contains) will be
appended to the result list being built. If the expression result is a false
value, the item will not be appended.
If writing any variable produces an error, so too will the lfilter command.
If the evaluation of the expression yields a TCL_ERROR then the overall
lfilter command will produce an error (possibly with the error info
extended). If the evaluation yields a TCL_RETURN, so too will the
lfilter command. If the evaluation yields a TCL_BREAK then the iteration
will stop. If the evaluation yields a TCL_CONTINUE, it will be treated as if
the expression evaluated to false. User-defined error codes will be passed
through.
If no error, return or user-defined condition occurs, the result of the lfilter command will be the list of (lead) items of list0 for which the expression evaluated to a true value, in order.
Options to be Considered
Whether to do an expr option for the (existing) dict filter command to
allow the use of filtering expressions instead of scripts.
Implementation
See the tip-735 branch.
Example Run
NB: This is a cut-n-paste from an interactive session running the implementation branch.
% lfilter x [lseq 100] {
# Is this value a square number?
round($x**0.5)**2 == $x
}
0 1 4 9 16 25 36 49 64 81
Illustrative Approximate Implementations
An approximate scripted version of this command is:
proc lfilter args {
set expression [lpop args end]
set v0 [lindex $args 0 0]
set cmd [list if $expression [list set $v0] else continue]
tailcall lmap {*}$args $cmd
}
A simpler version that only supports a single variable and list:
proc lfilter_simple {varName list expression} {
upvar 1 $varName var
set test [list expr $expression]
lmap var $list {
if {[uplevel 1 $test]} {set var} else continue
}
}
These are not expected to be full versions.
Copyright
This document has been placed in the public domain.