Tcl Part I · Module 4 · Operators and Expressions
in and ni: Membership Inside an Expression
A condition that asks whether a value is one of several accepted options comes up constantly — checking a mode, validating a user-supplied option, deciding whether a status counts as a failure. Tcl has two operators for exactly this, and they produce a truth value directly rather than an index you have to compare against minus one. They also make a decision you need to know about: the comparison is textual, which is the right default for the things people usually test and the wrong one for numbers.
Foundation10 min readTclPart IOperatorsMembershipValidation
Part I · Module 4 · Chapter 4.9 · in and ni: Membership Inside an Expression
1. The Two Operators
set mode fast
puts "mode in {fast slow} = [expr {$mode in {fast slow}}]"
puts "mode ni {fast slow} = [expr {$mode ni {fast slow}}]"
puts "bogus ni {fast slow} = [expr {"bogus" ni {fast slow}}]"mode in {fast slow} = 1
mode ni {fast slow} = 0
bogus ni {fast slow} = 1in is "is an element of". ni is "is not an element of" — it is simply the negation, provided so you do not have to write !(... in ...).
The right-hand side is a list. Here it is a braced literal, which Chapter 2.8 showed arrives as one word. It can equally be a variable:
set supported {fast slow careful}
set mode careful
puts "supported? [expr {$mode in $supported}]"supported? 12. Whole Elements Only
The match is against complete elements, never parts of them:
puts "\"fast\" in {fast slow} = [expr {"fast" in {fast slow}}]"
puts "\"fa\" in {fast slow} = [expr {"fa" in {fast slow}}]""fast" in {fast slow} = 1
"fa" in {fast slow} = 0fa is not an element, even though it is the start of one. If you want partial matching you want string match or a regular expression, which are Modules 5 and 13.
3. The Comparison Is Textual
This is the thing to know, and it follows the pattern of the whole module:
puts "1 in {1.0 2} = [expr {1 in {1.0 2}}]"
puts "1.0 in {1.0 2} = [expr {1.0 in {1.0 2}}]"
puts "10 in {9 10} = [expr {10 in {9 10}}]"1 in {1.0 2} = 0
1.0 in {1.0 2} = 1
10 in {9 10} = 1The first line is false. 1 and 1.0 are the same number and different text, and in compares text — exactly like eq in Chapter 4.4.
The second line is true because the characters match exactly. The third is true for the same reason: 10 is written identically in both places.
So membership succeeds only when the value is written the same way as the element.
4. Where It Earns Its Place
Validating an option:
set supported {fast slow careful}
foreach mode {fast turbo careful} {
if {$mode ni $supported} {
puts "unknown mode: $mode"
} else {
puts "running in $mode mode"
}
}running in fast mode
unknown mode: turbo
running in careful modeClassifying a status:
foreach status {PASS FAIL SKIP ERROR} {
set bad [expr {$status in {FAIL ERROR TIMEOUT}}]
puts [format "%-6s counts as failure: %s" $status $bad]
}PASS counts as failure: 0
FAIL counts as failure: 1
SKIP counts as failure: 0
ERROR counts as failure: 1The accepted set is written once, in one place, as a list. Adding TIMEOUT to the failure set is a one-word change rather than another || clause.
5. Compared With lsearch
lsearch (Module 6) can answer the same question, and it is worth seeing why in is better inside a condition:
set supported {fast slow careful}
set mode careful
puts "lsearch returns: [lsearch -exact $supported $mode]"
puts "as a condition: [expr {[lsearch -exact $supported $mode] != -1}]"
puts "with in: [expr {$mode in $supported}]"lsearch returns: 2
as a condition: 1
with in: 1lsearch returns an index, and -1 when there is no match. To use it in a condition you have to wrap it in a comparison against -1, which is noise — and it is easy to get wrong, because 0 is a perfectly good index and is also false.
in returns a truth value directly. Use lsearch when you want to know where something is; use in when you want to know whether it is there.
6. The Alternative for Many Cases
When the list grows and each case needs its own action, in is not the right shape any more:
if {$mode in {fast turbo quick}} {
...
}That is fine for one group. For several groups each doing something different, switch in Module 8 gives each case its own branch and reads far better than a chain of in tests.
7. Common Mistakes
Expecting numeric membership. §3. 1 in {1.0} is false.
Expecting partial matches. §2. fa is not in {fast}.
Writing !(... in ...). Use ni.
Using lsearch in a condition without != -1. An index of 0 is a match and is also false.
Forgetting the right side must be a valid list. A string that is not a well-formed list will error or behave unexpectedly — Module 6 covers what makes a list well-formed.
Chaining several in tests for different actions. Use switch.
8. Exercises
Exercise 1 — basic membership. Set a variable to slow and test it against {fast slow careful} with both in and ni. Then test turbo the same way.
Exercise 2 — whole elements. Predict and check: "car" in {carpet cart}, "cart" in {carpet cart}, "carpet cart" in {carpet cart}. Explain the third.
Exercise 3 — the numeric trap. Predict expr {5 in {5.0 6}} and expr {5 in {5 6}}. Then write a numeric membership test for 5 that works whichever way the list is written.
Exercise 4 — a validator. Write a loop over the modes quick, full, debug and turbo that prints ok or unknown mode: X, with the accepted set defined once as a variable.
Exercise 5 — replace lsearch. Rewrite if {[lsearch -exact $list $x] != -1} { ... } using in, and say what bug the original form invites.
Exercise 6 — failure statuses. Build a list of statuses that count as failures, then classify PASS, FAIL, WARN and ERROR against it. Add WARN to the set and confirm only one line changed.
9. Remember
intests membership;niis its negation. Both return1or0.- The right-hand side is a list — a braced literal or a variable holding one.
- Matching is on whole elements, never substrings.
- The comparison is textual, like
eq.1 in {1.0 2}is false. - So do not use it for numeric sets unless the text is written consistently.
- It is ideal for modes, statuses and option names, which are characters rather than quantities.
- Prefer it to
lsearchin a condition —lsearchreturns an index and needs!= -1. - When several cases need different actions, use
switch.
One chapter remains, and it explains why the comparison operators from earlier in this module should never be pointed at a measured number.
Where this fits
Part of the Tcl curriculum.
