iu2 2007-02-17 In a Python's program I've recently written there is a function that builds the GUI. An annoying thing about that function is that many GUI items and their data counterparts depend on a global application mode, which leads to many
ifs. All the
if commands make the code look unreadable and not so maintenance-friendly.
I thought of a tcl control structure for handling this situation more nicely. The example is the following, rather silly code
proc operateMode {} {
global theMode
puts {Alway performed}
if {$theMode eq "mode1"} {
foreach x {1 2 3} {
puts "mode1 $x"
}
}
# mode2 or mode3
if {$theMode in {mode2 mode3}} {
puts "Mode $theMode!"
puts "Do something for $theMode"
if {$theMode eq "mode2"} {
puts "mode is mode2"
} else {
puts "Mode 3 chosen"
}
}
if {$theMode eq "mode4"} {
puts "Now this is mode 4"
}
# end of modes
}
Trying it with the following script, let's call it
run example script # run example script
set allModes {mode1 mode2 mode3 mode4}
foreach theMode $allModes {
puts "Mode: $theMode"
operateMode
puts -----------------
}
gives
Mode: mode1
Alway performed
mode1 1
mode1 2
mode1 3
-----------------
Mode: mode2
Alway performed
Mode mode2!
Do something for mode2
mode is mode2
-----------------
Mode: mode3
Alway performed
Mode mode3!
Do something for mode3
Mode 3 chosen
-----------------
Mode: mode4
Alway performed
Now this is mode 4
-----------------
With the new control structure operateMode will be written as follows
proc operateMode {} {
modeAware $::theMode $::allModes {
global theMode
puts {Alway performed}
foreach x {1 2 3} {
puts "mode1 $x"
} - mode1
{
# mode2 or mode3
puts "Mode $theMode!"
puts "Do something for $theMode"
puts "mode is mode2" - mode2
puts "Mode 3 chosen" - mode3
} - mode2 mode3
puts "Now this is mode 4" - mode4
# end of modes
}
}
which focuses on
what to do rather than on
when to do it.
I wrote the code for
modeAware in stages.
1. A little bit shorter structureDefining a new proc "!!" let us rewrite the original opreateMode like this
proc operateMode {} {
global theMode
!! puts {Alway performed}
!! mode1 foreach x {1 2 3} {
puts "mode1 $x"
}
# mode2 or mode3
!! mode2 mode3 {
puts "Mode $theMode!"
puts "Do something for $theMode"
!! mode2 puts "mode is mode2"
!! mode3 puts "Mode 3 chosen"
}
!! mode4 puts "Now this is mode 4"
# end of mode
}
where "!!" is defined as an "if" replacement, and it filtes the command given to it according to the modes that appear on the line.
proc !! {args} {
set c -1
foreach m $args {
incr c
if {$m in $::allModes} {lappend chosenModes $m} break
}
if {[info exists chosenModes]} {
if {$::theMode in $chosenModes} {
set cmd [lrange $args $c end]
if {[llength $cmd] == 1} {set cmd [join $cmd]}
uplevel 1 $cmd
}
} else {
uplevel 1 $args
}
}
and yields the same result upon invoking the
run example script.
The proc "!!" already makes the code clearer because it cuts down lines and braces. Actually, I have used a couple of times deidcated short "if" replacements for "if" patterns that occured more than a few times in my code.
2. Shift the condition to the end of the lineWith "!!" changed to find the modes at the end of each command,
package require struct
proc !! {args} {
global allModes theMode
set c [llength $args]
foreach m [struct::list reverse $args] {
incr c -1
if {$m in $allModes} {lappend chosenModes $m} break
}
if {[info exists chosenModes]} {
if {$theMode in $chosenModes} {
set cmd [lrange $args 0 [expr {$c - 1}]]
if {[llength $cmd] == 1} {set cmd [join $cmd]}
uplevel 1 $cmd
}
} else { ;# no modes on the line
uplevel 1 $args
}
}
operateMode can be written as
proc operateMode {} {
global theMode
!! puts {Alway performed}
!! foreach x {1 2 3} {
puts "mode1 $x"
} - mode1
# mode2 or mode3
!! {
puts "Mode $theMode!"
puts "Do something for $theMode"
!! puts "mode is mode2" - mode2
!! puts "Mode 3 chosen" - mode3
} - mode2 mode3
!! puts "Now this is mode 4" - mode4
# end of mode
}
Anything can replace the "-" symbol, except for an empty string.. Also, If a non existing mode appears in the line, an error will occur (however, protecting theMode from being assigned an invalid mode is not handled).
3. Removing "!!" altogetherThe proc
modeAware will take a script and add "!!" for each line, then uplevel it. Since "!!" will not be written manually anymore, let's just change it to work on a given mode-argument instead of of global mode:
package require struct
proc !! {mode allowedModes args} {
set c [llength $args]
foreach m [struct::list reverse $args] {
incr c -1
if {$m in $allowedModes} {lappend chosenModes $m} break
}
if {[info exists chosenModes]} {
if {$mode in $chosenModes} {
set cmd [lrange $args 0 [expr {$c - 1}]]
if {[llength $cmd] == 1} {set cmd [join $cmd]}
uplevel 1 $cmd
}
} else {
uplevel 1 $args
}
}
And this is modeAware
proc modeAware {mode modes body} {
set lines [split $body \n]
set body ""
set cmd ""
foreach line $lines {
if {[string trim $line] ne ""} {
if {[regexp -lineanchor {^\s*#} $line]} { ;# a comment - do not add !! command
append body $line\n
} else {
append cmd $line
if {[string index $line end] ne "\\"} { ;# handle lines ending with a '\'
append body "!! $mode [list $modes] $cmd\n"
set cmd ""}}}} ;# a supporting editor would help a lot stuffing those braces...
uplevel 1 $body
}
which adds "!!" for each line, while not touching comment lines, and considering lines ending with a "\".
Comparing the first version of
operateMode and its final version:
proc operateMode {} { | proc operateMode {} {
global theMode | modeAware $::theMode $::allModes {
|
puts {Alway performed} | global theMode
if {$theMode eq "mode1"} { | puts {Alway performed}
foreach x {1 2 3} { | foreach x {1 2 3} {
puts "mode1 $x" | puts "mode1 $x"
} | } - mode1
} |
| {
# mode2 or mode3 | # mode2 or mode3
if {$theMode in {mode2 mode3}} { | puts "Mode $theMode!"
puts "Mode $theMode!" | puts "Do something for $theMode"
puts "Do something for $theMode" | puts "mode is mode2" - mode2
if {$theMode eq "mode2"} { | puts "Mode 3 chosen" - mode3
puts "mode is mode2" | } - mode2 mode3
} else { |
puts "Mode 3 chosen" | puts "Now this is mode 4" - mode4
} | # end of modes
} | }
| }
if {$theMode eq "mode4"} { |
puts "Now this is mode 4" |
} |
# end of modes |
} |
With
modeAware I can write all the operations I need, and then go over the code and just mark the sections that should operate on certain modes.